2025-12-19 09:30:16 +00:00
|
|
|
// Licensed to the .NET Foundation under one or more agreements.
|
|
|
|
|
// The .NET Foundation licenses this file to you under the MIT license.
|
|
|
|
|
|
2026-01-01 13:51:12 -05:00
|
|
|
using System;
|
2026-01-16 05:49:20 +00:00
|
|
|
using System.Collections.Generic;
|
2026-01-01 13:51:12 -05:00
|
|
|
using Microsoft.Maui.Controls;
|
2026-01-16 05:49:20 +00:00
|
|
|
using Microsoft.Maui.Controls.Shapes;
|
2026-01-16 05:30:52 +00:00
|
|
|
using Microsoft.Maui.Graphics;
|
2026-01-01 13:51:12 -05:00
|
|
|
using Microsoft.Maui.Platform.Linux.Handlers;
|
2025-12-19 09:30:16 +00:00
|
|
|
using SkiaSharp;
|
|
|
|
|
|
|
|
|
|
namespace Microsoft.Maui.Platform;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-12-21 13:26:56 -05:00
|
|
|
/// Skia-rendered border/frame container control with full XAML styling support.
|
2026-01-16 05:30:52 +00:00
|
|
|
/// Implements MAUI IBorderView interface patterns.
|
2025-12-19 09:30:16 +00:00
|
|
|
/// </summary>
|
|
|
|
|
public class SkiaBorder : SkiaLayoutView
|
|
|
|
|
{
|
2025-12-21 13:26:56 -05:00
|
|
|
#region BindableProperties
|
|
|
|
|
|
|
|
|
|
public static readonly BindableProperty StrokeThicknessProperty =
|
2026-01-16 05:30:52 +00:00
|
|
|
BindableProperty.Create(nameof(StrokeThickness), typeof(double), typeof(SkiaBorder), 1.0,
|
2026-01-01 13:51:12 -05:00
|
|
|
BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate());
|
2025-12-21 13:26:56 -05:00
|
|
|
|
|
|
|
|
public static readonly BindableProperty CornerRadiusProperty =
|
2026-01-16 05:30:52 +00:00
|
|
|
BindableProperty.Create(nameof(CornerRadius), typeof(double), typeof(SkiaBorder), 0.0,
|
2026-01-01 13:51:12 -05:00
|
|
|
BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate());
|
2025-12-21 13:26:56 -05:00
|
|
|
|
|
|
|
|
public static readonly BindableProperty StrokeProperty =
|
2026-01-16 05:30:52 +00:00
|
|
|
BindableProperty.Create(nameof(Stroke), typeof(Color), typeof(SkiaBorder), Colors.Black,
|
2026-01-01 13:51:12 -05:00
|
|
|
BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate());
|
2025-12-21 13:26:56 -05:00
|
|
|
|
2026-01-16 05:30:52 +00:00
|
|
|
public static readonly BindableProperty BorderPaddingProperty =
|
|
|
|
|
BindableProperty.Create(nameof(BorderPadding), typeof(Thickness), typeof(SkiaBorder), new Thickness(0),
|
2026-01-01 13:51:12 -05:00
|
|
|
BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBorder)b).InvalidateMeasure());
|
2025-12-21 13:26:56 -05:00
|
|
|
|
|
|
|
|
public static readonly BindableProperty HasShadowProperty =
|
|
|
|
|
BindableProperty.Create(nameof(HasShadow), typeof(bool), typeof(SkiaBorder), false,
|
2026-01-01 13:51:12 -05:00
|
|
|
BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate());
|
2025-12-21 13:26:56 -05:00
|
|
|
|
|
|
|
|
public static readonly BindableProperty ShadowColorProperty =
|
2026-01-16 05:30:52 +00:00
|
|
|
BindableProperty.Create(nameof(ShadowColor), typeof(Color), typeof(SkiaBorder), Color.FromRgba(0, 0, 0, 40),
|
2026-01-01 13:51:12 -05:00
|
|
|
BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate());
|
2025-12-21 13:26:56 -05:00
|
|
|
|
|
|
|
|
public static readonly BindableProperty ShadowBlurRadiusProperty =
|
2026-01-16 05:30:52 +00:00
|
|
|
BindableProperty.Create(nameof(ShadowBlurRadius), typeof(double), typeof(SkiaBorder), 4.0,
|
2026-01-01 13:51:12 -05:00
|
|
|
BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate());
|
2025-12-21 13:26:56 -05:00
|
|
|
|
|
|
|
|
public static readonly BindableProperty ShadowOffsetXProperty =
|
2026-01-16 05:30:52 +00:00
|
|
|
BindableProperty.Create(nameof(ShadowOffsetX), typeof(double), typeof(SkiaBorder), 2.0,
|
2026-01-01 13:51:12 -05:00
|
|
|
BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate());
|
2025-12-21 13:26:56 -05:00
|
|
|
|
|
|
|
|
public static readonly BindableProperty ShadowOffsetYProperty =
|
2026-01-16 05:30:52 +00:00
|
|
|
BindableProperty.Create(nameof(ShadowOffsetY), typeof(double), typeof(SkiaBorder), 2.0,
|
2026-01-01 13:51:12 -05:00
|
|
|
BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate());
|
2025-12-21 13:26:56 -05:00
|
|
|
|
2026-01-16 05:49:20 +00:00
|
|
|
public static readonly BindableProperty StrokeShapeProperty =
|
|
|
|
|
BindableProperty.Create(nameof(StrokeShape), typeof(IShape), typeof(SkiaBorder), null,
|
|
|
|
|
BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate());
|
|
|
|
|
|
|
|
|
|
public static readonly BindableProperty StrokeDashArrayProperty =
|
|
|
|
|
BindableProperty.Create(nameof(StrokeDashArray), typeof(DoubleCollection), typeof(SkiaBorder), null,
|
|
|
|
|
BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate());
|
|
|
|
|
|
|
|
|
|
public static readonly BindableProperty StrokeDashOffsetProperty =
|
|
|
|
|
BindableProperty.Create(nameof(StrokeDashOffset), typeof(double), typeof(SkiaBorder), 0.0,
|
|
|
|
|
BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate());
|
|
|
|
|
|
|
|
|
|
public static readonly BindableProperty StrokeLineCapProperty =
|
|
|
|
|
BindableProperty.Create(nameof(StrokeLineCap), typeof(LineCap), typeof(SkiaBorder), LineCap.Butt,
|
|
|
|
|
BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate());
|
|
|
|
|
|
|
|
|
|
public static readonly BindableProperty StrokeLineJoinProperty =
|
|
|
|
|
BindableProperty.Create(nameof(StrokeLineJoin), typeof(LineJoin), typeof(SkiaBorder), LineJoin.Miter,
|
|
|
|
|
BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate());
|
|
|
|
|
|
|
|
|
|
public static readonly BindableProperty StrokeMiterLimitProperty =
|
|
|
|
|
BindableProperty.Create(nameof(StrokeMiterLimit), typeof(double), typeof(SkiaBorder), 10.0,
|
|
|
|
|
BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate());
|
|
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
#endregion
|
|
|
|
|
|
2026-01-01 13:51:12 -05:00
|
|
|
private bool _isPressed;
|
|
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
#region Properties
|
2025-12-19 09:30:16 +00:00
|
|
|
|
2026-01-16 05:30:52 +00:00
|
|
|
public double StrokeThickness
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
2026-01-16 05:30:52 +00:00
|
|
|
get => (double)GetValue(StrokeThicknessProperty);
|
2025-12-21 13:26:56 -05:00
|
|
|
set => SetValue(StrokeThicknessProperty, value);
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-16 05:30:52 +00:00
|
|
|
public double CornerRadius
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
2026-01-16 05:30:52 +00:00
|
|
|
get => (double)GetValue(CornerRadiusProperty);
|
2025-12-21 13:26:56 -05:00
|
|
|
set => SetValue(CornerRadiusProperty, value);
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-16 05:30:52 +00:00
|
|
|
public Color Stroke
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
2026-01-16 05:30:52 +00:00
|
|
|
get => (Color)GetValue(StrokeProperty);
|
2025-12-21 13:26:56 -05:00
|
|
|
set => SetValue(StrokeProperty, value);
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-16 05:30:52 +00:00
|
|
|
public Thickness BorderPadding
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
2026-01-16 05:30:52 +00:00
|
|
|
get => (Thickness)GetValue(BorderPaddingProperty);
|
|
|
|
|
set => SetValue(BorderPaddingProperty, value);
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-16 05:30:52 +00:00
|
|
|
// Convenience properties for backward compatibility
|
|
|
|
|
public double PaddingLeft
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
2026-01-16 05:30:52 +00:00
|
|
|
get => BorderPadding.Left;
|
|
|
|
|
set => BorderPadding = new Thickness(value, BorderPadding.Top, BorderPadding.Right, BorderPadding.Bottom);
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-16 05:30:52 +00:00
|
|
|
public double PaddingTop
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
2026-01-16 05:30:52 +00:00
|
|
|
get => BorderPadding.Top;
|
|
|
|
|
set => BorderPadding = new Thickness(BorderPadding.Left, value, BorderPadding.Right, BorderPadding.Bottom);
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-16 05:30:52 +00:00
|
|
|
public double PaddingRight
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
2026-01-16 05:30:52 +00:00
|
|
|
get => BorderPadding.Right;
|
|
|
|
|
set => BorderPadding = new Thickness(BorderPadding.Left, BorderPadding.Top, value, BorderPadding.Bottom);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public double PaddingBottom
|
|
|
|
|
{
|
|
|
|
|
get => BorderPadding.Bottom;
|
|
|
|
|
set => BorderPadding = new Thickness(BorderPadding.Left, BorderPadding.Top, BorderPadding.Right, value);
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public bool HasShadow
|
|
|
|
|
{
|
2025-12-21 13:26:56 -05:00
|
|
|
get => (bool)GetValue(HasShadowProperty);
|
|
|
|
|
set => SetValue(HasShadowProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 05:30:52 +00:00
|
|
|
public Color ShadowColor
|
2025-12-21 13:26:56 -05:00
|
|
|
{
|
2026-01-16 05:30:52 +00:00
|
|
|
get => (Color)GetValue(ShadowColorProperty);
|
2025-12-21 13:26:56 -05:00
|
|
|
set => SetValue(ShadowColorProperty, value);
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-16 05:30:52 +00:00
|
|
|
public double ShadowBlurRadius
|
2025-12-21 13:26:56 -05:00
|
|
|
{
|
2026-01-16 05:30:52 +00:00
|
|
|
get => (double)GetValue(ShadowBlurRadiusProperty);
|
2025-12-21 13:26:56 -05:00
|
|
|
set => SetValue(ShadowBlurRadiusProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 05:30:52 +00:00
|
|
|
public double ShadowOffsetX
|
2025-12-21 13:26:56 -05:00
|
|
|
{
|
2026-01-16 05:30:52 +00:00
|
|
|
get => (double)GetValue(ShadowOffsetXProperty);
|
2025-12-21 13:26:56 -05:00
|
|
|
set => SetValue(ShadowOffsetXProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 05:30:52 +00:00
|
|
|
public double ShadowOffsetY
|
2025-12-21 13:26:56 -05:00
|
|
|
{
|
2026-01-16 05:30:52 +00:00
|
|
|
get => (double)GetValue(ShadowOffsetYProperty);
|
2025-12-21 13:26:56 -05:00
|
|
|
set => SetValue(ShadowOffsetYProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 05:49:20 +00:00
|
|
|
/// <summary>
|
|
|
|
|
/// Gets or sets the shape of the border stroke (Rectangle, RoundRectangle, Ellipse, etc.).
|
|
|
|
|
/// </summary>
|
|
|
|
|
public IShape? StrokeShape
|
|
|
|
|
{
|
|
|
|
|
get => (IShape?)GetValue(StrokeShapeProperty);
|
|
|
|
|
set => SetValue(StrokeShapeProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets or sets the dash pattern for the stroke.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public DoubleCollection? StrokeDashArray
|
|
|
|
|
{
|
|
|
|
|
get => (DoubleCollection?)GetValue(StrokeDashArrayProperty);
|
|
|
|
|
set => SetValue(StrokeDashArrayProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets or sets the offset into the dash pattern.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public double StrokeDashOffset
|
|
|
|
|
{
|
|
|
|
|
get => (double)GetValue(StrokeDashOffsetProperty);
|
|
|
|
|
set => SetValue(StrokeDashOffsetProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets or sets the cap style for the stroke line ends.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public LineCap StrokeLineCap
|
|
|
|
|
{
|
|
|
|
|
get => (LineCap)GetValue(StrokeLineCapProperty);
|
|
|
|
|
set => SetValue(StrokeLineCapProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets or sets the join style for stroke corners.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public LineJoin StrokeLineJoin
|
|
|
|
|
{
|
|
|
|
|
get => (LineJoin)GetValue(StrokeLineJoinProperty);
|
|
|
|
|
set => SetValue(StrokeLineJoinProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets or sets the miter limit for stroke joins.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public double StrokeMiterLimit
|
|
|
|
|
{
|
|
|
|
|
get => (double)GetValue(StrokeMiterLimitProperty);
|
|
|
|
|
set => SetValue(StrokeMiterLimitProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
#endregion
|
|
|
|
|
|
2026-01-01 13:51:12 -05:00
|
|
|
#region Events
|
|
|
|
|
|
|
|
|
|
public event EventHandler? Tapped;
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
2026-01-16 05:30:52 +00:00
|
|
|
#region Helper Methods
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Converts a MAUI Color to SkiaSharp SKColor.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private static SKColor ToSKColor(Color? color)
|
|
|
|
|
{
|
|
|
|
|
if (color == null) return SKColors.Transparent;
|
2026-01-17 03:36:37 +00:00
|
|
|
return color.ToSKColor();
|
2026-01-16 05:30:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
2026-01-01 13:51:12 -05:00
|
|
|
#region SetPadding Methods
|
|
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
/// <summary>
|
|
|
|
|
/// Sets uniform padding on all sides.
|
|
|
|
|
/// </summary>
|
2026-01-16 05:30:52 +00:00
|
|
|
public void SetPadding(double all)
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
2026-01-16 05:30:52 +00:00
|
|
|
BorderPadding = new Thickness(all);
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
/// <summary>
|
|
|
|
|
/// Sets padding with horizontal and vertical values.
|
|
|
|
|
/// </summary>
|
2026-01-16 05:30:52 +00:00
|
|
|
public void SetPadding(double horizontal, double vertical)
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
2026-01-16 05:30:52 +00:00
|
|
|
BorderPadding = new Thickness(horizontal, vertical);
|
2025-12-21 13:26:56 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Sets padding with individual values for each side.
|
|
|
|
|
/// </summary>
|
2026-01-16 05:30:52 +00:00
|
|
|
public void SetPadding(double left, double top, double right, double bottom)
|
2025-12-21 13:26:56 -05:00
|
|
|
{
|
2026-01-16 05:30:52 +00:00
|
|
|
BorderPadding = new Thickness(left, top, right, bottom);
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-01 13:51:12 -05:00
|
|
|
#endregion
|
|
|
|
|
|
2026-01-16 05:30:52 +00:00
|
|
|
#region Drawing
|
|
|
|
|
|
2026-01-16 05:49:20 +00:00
|
|
|
/// <summary>
|
|
|
|
|
/// Converts LineCap to SKStrokeCap.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private static SKStrokeCap ToSKStrokeCap(LineCap lineCap)
|
|
|
|
|
{
|
|
|
|
|
return lineCap switch
|
|
|
|
|
{
|
|
|
|
|
LineCap.Round => SKStrokeCap.Round,
|
|
|
|
|
LineCap.Square => SKStrokeCap.Square,
|
|
|
|
|
_ => SKStrokeCap.Butt
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Converts LineJoin to SKStrokeJoin.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private static SKStrokeJoin ToSKStrokeJoin(LineJoin lineJoin)
|
|
|
|
|
{
|
|
|
|
|
return lineJoin switch
|
|
|
|
|
{
|
|
|
|
|
LineJoin.Round => SKStrokeJoin.Round,
|
|
|
|
|
LineJoin.Bevel => SKStrokeJoin.Bevel,
|
|
|
|
|
_ => SKStrokeJoin.Miter
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Creates an SKPath for the border based on StrokeShape.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private SKPath CreateShapePath(SKRect rect, float defaultCornerRadius)
|
|
|
|
|
{
|
|
|
|
|
var path = new SKPath();
|
|
|
|
|
|
|
|
|
|
if (StrokeShape is RoundRectangle roundRect)
|
|
|
|
|
{
|
|
|
|
|
// Use RoundRectangle's corner radii
|
|
|
|
|
var cr = roundRect.CornerRadius;
|
|
|
|
|
var radii = new SKPoint[]
|
|
|
|
|
{
|
|
|
|
|
new SKPoint((float)cr.TopLeft, (float)cr.TopLeft),
|
|
|
|
|
new SKPoint((float)cr.TopRight, (float)cr.TopRight),
|
|
|
|
|
new SKPoint((float)cr.BottomRight, (float)cr.BottomRight),
|
|
|
|
|
new SKPoint((float)cr.BottomLeft, (float)cr.BottomLeft)
|
|
|
|
|
};
|
|
|
|
|
var skRoundRect = new SKRoundRect();
|
|
|
|
|
skRoundRect.SetRectRadii(rect, radii);
|
|
|
|
|
path.AddRoundRect(skRoundRect);
|
|
|
|
|
}
|
|
|
|
|
else if (StrokeShape is Ellipse)
|
|
|
|
|
{
|
|
|
|
|
path.AddOval(rect);
|
|
|
|
|
}
|
|
|
|
|
else if (StrokeShape is Rectangle)
|
|
|
|
|
{
|
|
|
|
|
path.AddRect(rect);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// Default: use CornerRadius property
|
|
|
|
|
if (defaultCornerRadius > 0)
|
|
|
|
|
{
|
|
|
|
|
path.AddRoundRect(rect, defaultCornerRadius, defaultCornerRadius);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
path.AddRect(rect);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return path;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-17 15:06:39 +00:00
|
|
|
/// <summary>
|
|
|
|
|
/// Override to skip rectangular background - OnDraw handles it with the correct shape.
|
|
|
|
|
/// </summary>
|
|
|
|
|
protected override void DrawBackground(SKCanvas canvas, SKRect bounds)
|
|
|
|
|
{
|
|
|
|
|
// Don't draw rectangular background - OnDraw draws background with shape path
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 09:30:16 +00:00
|
|
|
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
|
|
|
|
|
{
|
2026-01-16 05:30:52 +00:00
|
|
|
float strokeThickness = (float)StrokeThickness;
|
|
|
|
|
float cornerRadius = (float)CornerRadius;
|
2025-12-21 13:26:56 -05:00
|
|
|
|
2025-12-19 09:30:16 +00:00
|
|
|
var borderRect = new SKRect(
|
2026-01-01 13:51:12 -05:00
|
|
|
bounds.Left + strokeThickness / 2f,
|
|
|
|
|
bounds.Top + strokeThickness / 2f,
|
|
|
|
|
bounds.Right - strokeThickness / 2f,
|
|
|
|
|
bounds.Bottom - strokeThickness / 2f);
|
2025-12-19 09:30:16 +00:00
|
|
|
|
2026-01-16 05:49:20 +00:00
|
|
|
// Create the shape path
|
|
|
|
|
using var shapePath = CreateShapePath(borderRect, cornerRadius);
|
|
|
|
|
|
2025-12-19 09:30:16 +00:00
|
|
|
// Draw shadow if enabled
|
2025-12-21 13:26:56 -05:00
|
|
|
if (HasShadow)
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
|
|
|
|
using var shadowPaint = new SKPaint
|
|
|
|
|
{
|
2026-01-16 05:30:52 +00:00
|
|
|
Color = ToSKColor(ShadowColor),
|
|
|
|
|
MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, (float)ShadowBlurRadius),
|
2025-12-19 09:30:16 +00:00
|
|
|
Style = SKPaintStyle.Fill
|
|
|
|
|
};
|
2026-01-16 05:49:20 +00:00
|
|
|
canvas.Save();
|
|
|
|
|
canvas.Translate((float)ShadowOffsetX, (float)ShadowOffsetY);
|
|
|
|
|
canvas.DrawPath(shapePath, shadowPaint);
|
|
|
|
|
canvas.Restore();
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Draw background
|
|
|
|
|
using var bgPaint = new SKPaint
|
|
|
|
|
{
|
2026-01-17 03:10:29 +00:00
|
|
|
Color = GetEffectiveBackgroundColor(),
|
2025-12-19 09:30:16 +00:00
|
|
|
Style = SKPaintStyle.Fill,
|
|
|
|
|
IsAntialias = true
|
|
|
|
|
};
|
2026-01-16 05:49:20 +00:00
|
|
|
canvas.DrawPath(shapePath, bgPaint);
|
2025-12-19 09:30:16 +00:00
|
|
|
|
|
|
|
|
// Draw border
|
2026-01-01 13:51:12 -05:00
|
|
|
if (strokeThickness > 0f)
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
|
|
|
|
using var borderPaint = new SKPaint
|
|
|
|
|
{
|
2026-01-16 05:30:52 +00:00
|
|
|
Color = ToSKColor(Stroke),
|
2025-12-19 09:30:16 +00:00
|
|
|
Style = SKPaintStyle.Stroke,
|
2025-12-21 13:26:56 -05:00
|
|
|
StrokeWidth = strokeThickness,
|
2026-01-16 05:49:20 +00:00
|
|
|
IsAntialias = true,
|
|
|
|
|
StrokeCap = ToSKStrokeCap(StrokeLineCap),
|
|
|
|
|
StrokeJoin = ToSKStrokeJoin(StrokeLineJoin),
|
|
|
|
|
StrokeMiter = (float)StrokeMiterLimit
|
2025-12-19 09:30:16 +00:00
|
|
|
};
|
2026-01-16 05:49:20 +00:00
|
|
|
|
|
|
|
|
// Apply dash pattern if specified
|
|
|
|
|
if (StrokeDashArray != null && StrokeDashArray.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
var dashArray = new float[StrokeDashArray.Count];
|
|
|
|
|
for (int i = 0; i < StrokeDashArray.Count; i++)
|
|
|
|
|
{
|
|
|
|
|
dashArray[i] = (float)(StrokeDashArray[i] * strokeThickness);
|
|
|
|
|
}
|
|
|
|
|
borderPaint.PathEffect = SKPathEffect.CreateDash(dashArray, (float)(StrokeDashOffset * strokeThickness));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
canvas.DrawPath(shapePath, borderPaint);
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-17 08:06:22 +00:00
|
|
|
// Clip to shape and draw children
|
|
|
|
|
canvas.Save();
|
|
|
|
|
canvas.ClipPath(shapePath);
|
2025-12-19 09:30:16 +00:00
|
|
|
foreach (var child in Children)
|
|
|
|
|
{
|
|
|
|
|
if (child.IsVisible)
|
|
|
|
|
{
|
|
|
|
|
child.Draw(canvas);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-17 08:06:22 +00:00
|
|
|
canvas.Restore();
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-16 05:30:52 +00:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Layout
|
|
|
|
|
|
2025-12-19 09:30:16 +00:00
|
|
|
protected override SKRect GetContentBounds()
|
|
|
|
|
{
|
2026-01-17 05:22:37 +00:00
|
|
|
return GetContentBounds(new SKRect((float)Bounds.Left, (float)Bounds.Top, (float)Bounds.Right, (float)Bounds.Bottom));
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected new SKRect GetContentBounds(SKRect bounds)
|
|
|
|
|
{
|
2026-01-16 05:30:52 +00:00
|
|
|
float strokeThickness = (float)StrokeThickness;
|
|
|
|
|
var padding = BorderPadding;
|
2025-12-19 09:30:16 +00:00
|
|
|
return new SKRect(
|
2026-01-16 05:30:52 +00:00
|
|
|
bounds.Left + (float)padding.Left + strokeThickness,
|
|
|
|
|
bounds.Top + (float)padding.Top + strokeThickness,
|
|
|
|
|
bounds.Right - (float)padding.Right - strokeThickness,
|
|
|
|
|
bounds.Bottom - (float)padding.Bottom - strokeThickness);
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-17 05:22:37 +00:00
|
|
|
protected override Size MeasureOverride(Size availableSize)
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
2026-01-16 05:30:52 +00:00
|
|
|
float strokeThickness = (float)StrokeThickness;
|
|
|
|
|
var padding = BorderPadding;
|
|
|
|
|
float paddingWidth = (float)(padding.Left + padding.Right) + strokeThickness * 2f;
|
|
|
|
|
float paddingHeight = (float)(padding.Top + padding.Bottom) + strokeThickness * 2f;
|
2025-12-21 13:26:56 -05:00
|
|
|
|
|
|
|
|
// Respect explicit size requests
|
2026-01-17 05:22:37 +00:00
|
|
|
var requestedWidth = WidthRequest >= 0.0 ? (float)WidthRequest : (float)availableSize.Width;
|
|
|
|
|
var requestedHeight = HeightRequest >= 0.0 ? (float)HeightRequest : (float)availableSize.Height;
|
2025-12-19 09:30:16 +00:00
|
|
|
|
2026-01-17 05:22:37 +00:00
|
|
|
var childAvailable = new Size(
|
2026-01-01 13:51:12 -05:00
|
|
|
Math.Max(0f, requestedWidth - paddingWidth),
|
|
|
|
|
Math.Max(0f, requestedHeight - paddingHeight));
|
2025-12-19 09:30:16 +00:00
|
|
|
|
2026-01-17 05:22:37 +00:00
|
|
|
var maxChildSize = Size.Zero;
|
2025-12-19 09:30:16 +00:00
|
|
|
|
|
|
|
|
foreach (var child in Children)
|
|
|
|
|
{
|
|
|
|
|
var childSize = child.Measure(childAvailable);
|
2026-01-17 05:22:37 +00:00
|
|
|
maxChildSize = new Size(
|
2025-12-19 09:30:16 +00:00
|
|
|
Math.Max(maxChildSize.Width, childSize.Width),
|
|
|
|
|
Math.Max(maxChildSize.Height, childSize.Height));
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
// Use requested size if set, otherwise use child size + padding
|
2026-01-17 05:22:37 +00:00
|
|
|
var width = WidthRequest >= 0.0 ? (float)WidthRequest : (float)maxChildSize.Width + paddingWidth;
|
|
|
|
|
var height = HeightRequest >= 0.0 ? (float)HeightRequest : (float)maxChildSize.Height + paddingHeight;
|
2025-12-21 13:26:56 -05:00
|
|
|
|
2026-01-17 05:22:37 +00:00
|
|
|
return new Size(width, height);
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-17 05:22:37 +00:00
|
|
|
protected override Rect ArrangeOverride(Rect bounds)
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
2026-01-17 05:22:37 +00:00
|
|
|
var contentBounds = GetContentBounds(new SKRect((float)bounds.Left, (float)bounds.Top, (float)bounds.Right, (float)bounds.Bottom));
|
2025-12-19 09:30:16 +00:00
|
|
|
|
|
|
|
|
foreach (var child in Children)
|
|
|
|
|
{
|
2025-12-21 13:26:56 -05:00
|
|
|
// Apply child's margin
|
|
|
|
|
var margin = child.Margin;
|
2026-01-17 05:22:37 +00:00
|
|
|
var marginedBounds = new Rect(
|
|
|
|
|
contentBounds.Left + margin.Left,
|
|
|
|
|
contentBounds.Top + margin.Top,
|
|
|
|
|
contentBounds.Width - margin.Left - margin.Right,
|
|
|
|
|
contentBounds.Height - margin.Top - margin.Bottom);
|
2025-12-21 13:26:56 -05:00
|
|
|
child.Arrange(marginedBounds);
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return bounds;
|
|
|
|
|
}
|
2026-01-01 13:51:12 -05:00
|
|
|
|
2026-01-16 05:30:52 +00:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Input Handling
|
|
|
|
|
|
2026-01-01 13:51:12 -05:00
|
|
|
private bool HasTapGestureRecognizers()
|
|
|
|
|
{
|
|
|
|
|
if (MauiView?.GestureRecognizers == null)
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach (var gestureRecognizer in MauiView.GestureRecognizers)
|
|
|
|
|
{
|
|
|
|
|
if (gestureRecognizer is TapGestureRecognizer)
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override SkiaView? HitTest(float x, float y)
|
|
|
|
|
{
|
|
|
|
|
if (IsVisible && IsEnabled)
|
|
|
|
|
{
|
|
|
|
|
var bounds = Bounds;
|
2026-01-17 05:22:37 +00:00
|
|
|
if (bounds.Contains(x, y))
|
2026-01-01 13:51:12 -05:00
|
|
|
{
|
|
|
|
|
if (HasTapGestureRecognizers())
|
|
|
|
|
{
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
return base.HitTest(x, y);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override void OnPointerPressed(PointerEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (HasTapGestureRecognizers())
|
|
|
|
|
{
|
|
|
|
|
_isPressed = true;
|
|
|
|
|
e.Handled = true;
|
|
|
|
|
if (MauiView != null)
|
|
|
|
|
{
|
|
|
|
|
GestureManager.ProcessPointerDown(MauiView, e.X, e.Y);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
base.OnPointerPressed(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override void OnPointerReleased(PointerEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (_isPressed)
|
|
|
|
|
{
|
|
|
|
|
_isPressed = false;
|
|
|
|
|
e.Handled = true;
|
|
|
|
|
if (MauiView != null)
|
|
|
|
|
{
|
|
|
|
|
GestureManager.ProcessPointerUp(MauiView, e.X, e.Y);
|
|
|
|
|
}
|
|
|
|
|
Tapped?.Invoke(this, EventArgs.Empty);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
base.OnPointerReleased(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override void OnPointerExited(PointerEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
base.OnPointerExited(e);
|
|
|
|
|
_isPressed = false;
|
|
|
|
|
}
|
2026-01-16 05:30:52 +00:00
|
|
|
|
|
|
|
|
#endregion
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|