diff --git a/Views/SkiaView.cs b/Views/SkiaView.cs index d74d4c2..0c46bfb 100644 --- a/Views/SkiaView.cs +++ b/Views/SkiaView.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Maui.Controls; +using Microsoft.Maui.Controls.Shapes; using Microsoft.Maui.Platform.Linux; using Microsoft.Maui.Platform.Linux.Handlers; using Microsoft.Maui.Platform.Linux.Rendering; @@ -333,6 +334,128 @@ public abstract class SkiaView : BindableObject, IDisposable 0.5, propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate()); + /// + /// Bindable property for InputTransparent. + /// When true, the view does not receive input events and they pass through to views below. + /// + public static readonly BindableProperty InputTransparentProperty = + BindableProperty.Create( + nameof(InputTransparent), + typeof(bool), + typeof(SkiaView), + false); + + /// + /// Bindable property for FlowDirection. + /// Controls the layout direction for RTL language support. + /// + public static readonly BindableProperty FlowDirectionProperty = + BindableProperty.Create( + nameof(FlowDirection), + typeof(FlowDirection), + typeof(SkiaView), + FlowDirection.MatchParent, + propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure()); + + /// + /// Bindable property for ZIndex. + /// Controls the rendering order within a layout. + /// + public static readonly BindableProperty ZIndexProperty = + BindableProperty.Create( + nameof(ZIndex), + typeof(int), + typeof(SkiaView), + 0, + propertyChanged: (b, o, n) => ((SkiaView)b).Parent?.Invalidate()); + + /// + /// Bindable property for MaximumWidthRequest. + /// + public static readonly BindableProperty MaximumWidthRequestProperty = + BindableProperty.Create( + nameof(MaximumWidthRequest), + typeof(double), + typeof(SkiaView), + double.PositiveInfinity, + propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure()); + + /// + /// Bindable property for MaximumHeightRequest. + /// + public static readonly BindableProperty MaximumHeightRequestProperty = + BindableProperty.Create( + nameof(MaximumHeightRequest), + typeof(double), + typeof(SkiaView), + double.PositiveInfinity, + propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure()); + + /// + /// Bindable property for AutomationId. + /// Used for UI testing and accessibility. + /// + public static readonly BindableProperty AutomationIdProperty = + BindableProperty.Create( + nameof(AutomationId), + typeof(string), + typeof(SkiaView), + string.Empty); + + /// + /// Bindable property for Padding. + /// + public static readonly BindableProperty PaddingProperty = + BindableProperty.Create( + nameof(Padding), + typeof(Thickness), + typeof(SkiaView), + default(Thickness), + propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure()); + + /// + /// Bindable property for Background (Brush). + /// + public static readonly BindableProperty BackgroundProperty = + BindableProperty.Create( + nameof(Background), + typeof(Brush), + typeof(SkiaView), + null, + propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate()); + + /// + /// Bindable property for Clip geometry. + /// + public static readonly BindableProperty ClipProperty = + BindableProperty.Create( + nameof(Clip), + typeof(Geometry), + typeof(SkiaView), + null, + propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate()); + + /// + /// Bindable property for Shadow. + /// + public static readonly BindableProperty ShadowProperty = + BindableProperty.Create( + nameof(Shadow), + typeof(Shadow), + typeof(SkiaView), + null, + propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate()); + + /// + /// Bindable property for Visual. + /// + public static readonly BindableProperty VisualProperty = + BindableProperty.Create( + nameof(Visual), + typeof(IVisual), + typeof(SkiaView), + VisualMarker.Default); + #endregion private bool _disposed; @@ -613,6 +736,107 @@ public abstract class SkiaView : BindableObject, IDisposable set => SetValue(AnchorYProperty, value); } + /// + /// Gets or sets whether this view is transparent to input. + /// When true, input events pass through to views below. + /// + public bool InputTransparent + { + get => (bool)GetValue(InputTransparentProperty); + set => SetValue(InputTransparentProperty, value); + } + + /// + /// Gets or sets the flow direction for RTL support. + /// + public FlowDirection FlowDirection + { + get => (FlowDirection)GetValue(FlowDirectionProperty); + set => SetValue(FlowDirectionProperty, value); + } + + /// + /// Gets or sets the Z-index for rendering order. + /// Higher values render on top of lower values. + /// + public int ZIndex + { + get => (int)GetValue(ZIndexProperty); + set => SetValue(ZIndexProperty, value); + } + + /// + /// Gets or sets the maximum width request. + /// + public double MaximumWidthRequest + { + get => (double)GetValue(MaximumWidthRequestProperty); + set => SetValue(MaximumWidthRequestProperty, value); + } + + /// + /// Gets or sets the maximum height request. + /// + public double MaximumHeightRequest + { + get => (double)GetValue(MaximumHeightRequestProperty); + set => SetValue(MaximumHeightRequestProperty, value); + } + + /// + /// Gets or sets the automation ID for UI testing. + /// + public string AutomationId + { + get => (string)GetValue(AutomationIdProperty); + set => SetValue(AutomationIdProperty, value); + } + + /// + /// Gets or sets the padding inside the view. + /// + public Thickness Padding + { + get => (Thickness)GetValue(PaddingProperty); + set => SetValue(PaddingProperty, value); + } + + /// + /// Gets or sets the background brush. + /// + public Brush? Background + { + get => (Brush?)GetValue(BackgroundProperty); + set => SetValue(BackgroundProperty, value); + } + + /// + /// Gets or sets the clip geometry. + /// + public Geometry? Clip + { + get => (Geometry?)GetValue(ClipProperty); + set => SetValue(ClipProperty, value); + } + + /// + /// Gets or sets the shadow. + /// + public Shadow? Shadow + { + get => (Shadow?)GetValue(ShadowProperty); + set => SetValue(ShadowProperty, value); + } + + /// + /// Gets or sets the visual style. + /// + public IVisual Visual + { + get => (IVisual)GetValue(VisualProperty); + set => SetValue(VisualProperty, value); + } + /// /// Gets or sets the cursor type when hovering over this view. /// @@ -865,13 +1089,21 @@ public abstract class SkiaView : BindableObject, IDisposable canvas.SaveLayer(new SKPaint { Color = SKColors.White.WithAlpha((byte)(Opacity * 255)) }); } - // Draw background at absolute bounds - if (BackgroundColor != SKColors.Transparent) + // Draw shadow if set + if (Shadow != null) { - using var paint = new SKPaint { Color = BackgroundColor }; - canvas.DrawRect(Bounds, paint); + DrawShadow(canvas, Bounds); } + // Apply clip geometry if set + if (Clip != null) + { + ApplyClip(canvas, Bounds); + } + + // Draw background at absolute bounds + DrawBackground(canvas, Bounds); + // Draw content at absolute bounds OnDraw(canvas, Bounds); @@ -896,6 +1128,166 @@ public abstract class SkiaView : BindableObject, IDisposable { } + /// + /// Draws the shadow for this view. + /// + protected virtual void DrawShadow(SKCanvas canvas, SKRect bounds) + { + if (Shadow == null) return; + + var shadowColor = Shadow.Brush is SolidColorBrush scb + ? new SKColor( + (byte)(scb.Color.Red * 255), + (byte)(scb.Color.Green * 255), + (byte)(scb.Color.Blue * 255), + (byte)(scb.Color.Alpha * 255 * Shadow.Opacity)) + : new SKColor(0, 0, 0, (byte)(255 * Shadow.Opacity)); + + using var shadowPaint = new SKPaint + { + Color = shadowColor, + IsAntialias = true, + MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, (float)Shadow.Radius / 2) + }; + + var shadowBounds = new SKRect( + bounds.Left + (float)Shadow.Offset.X, + bounds.Top + (float)Shadow.Offset.Y, + bounds.Right + (float)Shadow.Offset.X, + bounds.Bottom + (float)Shadow.Offset.Y); + + canvas.DrawRect(shadowBounds, shadowPaint); + } + + /// + /// Applies the clip geometry to the canvas. + /// + protected virtual void ApplyClip(SKCanvas canvas, SKRect bounds) + { + if (Clip == null) return; + + // Convert MAUI Geometry to SkiaSharp path + var path = ConvertGeometryToPath(Clip, bounds); + if (path != null) + { + canvas.ClipPath(path); + } + } + + /// + /// Converts a MAUI Geometry to a SkiaSharp path. + /// + private SKPath? ConvertGeometryToPath(Geometry geometry, SKRect bounds) + { + var path = new SKPath(); + + if (geometry is RectangleGeometry rect) + { + var r = rect.Rect; + path.AddRect(new SKRect( + bounds.Left + (float)r.Left, + bounds.Top + (float)r.Top, + bounds.Left + (float)r.Right, + bounds.Top + (float)r.Bottom)); + } + else if (geometry is EllipseGeometry ellipse) + { + path.AddOval(new SKRect( + bounds.Left + (float)(ellipse.Center.X - ellipse.RadiusX), + bounds.Top + (float)(ellipse.Center.Y - ellipse.RadiusY), + bounds.Left + (float)(ellipse.Center.X + ellipse.RadiusX), + bounds.Top + (float)(ellipse.Center.Y + ellipse.RadiusY))); + } + else if (geometry is RoundRectangleGeometry roundRect) + { + var r = roundRect.Rect; + var cr = roundRect.CornerRadius; + var skRect = new SKRect( + bounds.Left + (float)r.Left, + bounds.Top + (float)r.Top, + bounds.Left + (float)r.Right, + bounds.Top + (float)r.Bottom); + var skRoundRect = new SKRoundRect(); + skRoundRect.SetRectRadii(skRect, new[] + { + 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) + }); + path.AddRoundRect(skRoundRect); + } + // Add more geometry types as needed + + return path; + } + + /// + /// Draws the background (color or brush) for this view. + /// + protected virtual void DrawBackground(SKCanvas canvas, SKRect bounds) + { + // First try to use Background brush + if (Background != null) + { + using var paint = new SKPaint { IsAntialias = true }; + + if (Background is SolidColorBrush scb) + { + paint.Color = new SKColor( + (byte)(scb.Color.Red * 255), + (byte)(scb.Color.Green * 255), + (byte)(scb.Color.Blue * 255), + (byte)(scb.Color.Alpha * 255)); + canvas.DrawRect(bounds, paint); + } + else if (Background is LinearGradientBrush lgb) + { + var start = new SKPoint( + bounds.Left + (float)(lgb.StartPoint.X * bounds.Width), + bounds.Top + (float)(lgb.StartPoint.Y * bounds.Height)); + var end = new SKPoint( + bounds.Left + (float)(lgb.EndPoint.X * bounds.Width), + bounds.Top + (float)(lgb.EndPoint.Y * bounds.Height)); + + var colors = lgb.GradientStops.Select(s => + new SKColor( + (byte)(s.Color.Red * 255), + (byte)(s.Color.Green * 255), + (byte)(s.Color.Blue * 255), + (byte)(s.Color.Alpha * 255))).ToArray(); + var positions = lgb.GradientStops.Select(s => s.Offset).ToArray(); + + paint.Shader = SKShader.CreateLinearGradient(start, end, colors, positions, SKShaderTileMode.Clamp); + canvas.DrawRect(bounds, paint); + } + else if (Background is RadialGradientBrush rgb) + { + var center = new SKPoint( + bounds.Left + (float)(rgb.Center.X * bounds.Width), + bounds.Top + (float)(rgb.Center.Y * bounds.Height)); + var radius = (float)(rgb.Radius * Math.Max(bounds.Width, bounds.Height)); + + var colors = rgb.GradientStops.Select(s => + new SKColor( + (byte)(s.Color.Red * 255), + (byte)(s.Color.Green * 255), + (byte)(s.Color.Blue * 255), + (byte)(s.Color.Alpha * 255))).ToArray(); + var positions = rgb.GradientStops.Select(s => s.Offset).ToArray(); + + paint.Shader = SKShader.CreateRadialGradient(center, radius, colors, positions, SKShaderTileMode.Clamp); + canvas.DrawRect(bounds, paint); + } + } + // Fall back to BackgroundColor + else if (BackgroundColor != SKColors.Transparent) + { + using var paint = new SKPaint { Color = BackgroundColor }; + canvas.DrawRect(bounds, paint); + } + } + /// /// Called when the bounds change. /// @@ -968,6 +1360,10 @@ public abstract class SkiaView : BindableObject, IDisposable return hit; } + // If InputTransparent, don't capture input - let it pass through + if (InputTransparent) + return null; + return this; }