// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Shapes;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform.Linux;
using Microsoft.Maui.Platform.Linux.Handlers;
using Microsoft.Maui.Platform.Linux.Native;
using Microsoft.Maui.Platform.Linux.Rendering;
using Microsoft.Maui.Platform.Linux.Services;
using Microsoft.Maui.Platform.Linux.Window;
using SkiaSharp;
namespace Microsoft.Maui.Platform;
///
/// Base class for all Skia-rendered views on Linux.
/// Inherits from BindableObject to enable XAML styling, data binding, and Visual State Manager.
/// Implements IAccessible for screen reader support.
///
public abstract partial class SkiaView : BindableObject, IDisposable, IAccessible
{
#region BindableProperties
///
/// Bindable property for IsVisible.
///
public static readonly BindableProperty IsVisibleProperty =
BindableProperty.Create(
nameof(IsVisible),
typeof(bool),
typeof(SkiaView),
true,
propertyChanged: (b, o, n) => ((SkiaView)b).OnVisibilityChanged());
///
/// Bindable property for IsEnabled.
///
public static readonly BindableProperty IsEnabledProperty =
BindableProperty.Create(
nameof(IsEnabled),
typeof(bool),
typeof(SkiaView),
true,
propertyChanged: (b, o, n) => ((SkiaView)b).OnEnabledChanged());
///
/// Bindable property for Opacity.
///
public static readonly BindableProperty OpacityProperty =
BindableProperty.Create(
nameof(Opacity),
typeof(float),
typeof(SkiaView),
1.0f,
coerceValue: (b, v) => Math.Clamp((float)v, 0f, 1f),
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
///
/// Bindable property for BackgroundColor.
/// Uses Microsoft.Maui.Graphics.Color for MAUI compliance.
///
public static readonly BindableProperty BackgroundColorProperty =
BindableProperty.Create(
nameof(BackgroundColor),
typeof(Color),
typeof(SkiaView),
null,
propertyChanged: (b, o, n) => ((SkiaView)b).OnBackgroundColorChanged());
///
/// Bindable property for WidthRequest.
///
public static readonly BindableProperty WidthRequestProperty =
BindableProperty.Create(
nameof(WidthRequest),
typeof(double),
typeof(SkiaView),
-1.0,
propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure());
///
/// Bindable property for HeightRequest.
///
public static readonly BindableProperty HeightRequestProperty =
BindableProperty.Create(
nameof(HeightRequest),
typeof(double),
typeof(SkiaView),
-1.0,
propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure());
///
/// Bindable property for MinimumWidthRequest.
/// Default is -1 (unset) to match MAUI View.MinimumWidthRequest.
///
public static readonly BindableProperty MinimumWidthRequestProperty =
BindableProperty.Create(
nameof(MinimumWidthRequest),
typeof(double),
typeof(SkiaView),
-1.0,
propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure());
///
/// Bindable property for MinimumHeightRequest.
/// Default is -1 (unset) to match MAUI View.MinimumHeightRequest.
///
public static readonly BindableProperty MinimumHeightRequestProperty =
BindableProperty.Create(
nameof(MinimumHeightRequest),
typeof(double),
typeof(SkiaView),
-1.0,
propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure());
///
/// Bindable property for IsFocusable.
///
public static readonly BindableProperty IsFocusableProperty =
BindableProperty.Create(
nameof(IsFocusable),
typeof(bool),
typeof(SkiaView),
false);
///
/// Bindable property for Margin.
///
public static readonly BindableProperty MarginProperty =
BindableProperty.Create(
nameof(Margin),
typeof(Thickness),
typeof(SkiaView),
default(Thickness),
propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure());
///
/// Bindable property for HorizontalOptions.
///
public static readonly BindableProperty HorizontalOptionsProperty =
BindableProperty.Create(
nameof(HorizontalOptions),
typeof(LayoutOptions),
typeof(SkiaView),
LayoutOptions.Fill,
propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure());
///
/// Bindable property for VerticalOptions.
///
public static readonly BindableProperty VerticalOptionsProperty =
BindableProperty.Create(
nameof(VerticalOptions),
typeof(LayoutOptions),
typeof(SkiaView),
LayoutOptions.Fill,
propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure());
///
/// Bindable property for Name (used for template child lookup).
///
public static readonly BindableProperty NameProperty =
BindableProperty.Create(
nameof(Name),
typeof(string),
typeof(SkiaView),
string.Empty);
///
/// Bindable property for Scale.
///
public static readonly BindableProperty ScaleProperty =
BindableProperty.Create(
nameof(Scale),
typeof(double),
typeof(SkiaView),
1.0,
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
///
/// Bindable property for ScaleX.
///
public static readonly BindableProperty ScaleXProperty =
BindableProperty.Create(
nameof(ScaleX),
typeof(double),
typeof(SkiaView),
1.0,
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
///
/// Bindable property for ScaleY.
///
public static readonly BindableProperty ScaleYProperty =
BindableProperty.Create(
nameof(ScaleY),
typeof(double),
typeof(SkiaView),
1.0,
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
///
/// Bindable property for Rotation.
///
public static readonly BindableProperty RotationProperty =
BindableProperty.Create(
nameof(Rotation),
typeof(double),
typeof(SkiaView),
0.0,
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
///
/// Bindable property for RotationX.
///
public static readonly BindableProperty RotationXProperty =
BindableProperty.Create(
nameof(RotationX),
typeof(double),
typeof(SkiaView),
0.0,
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
///
/// Bindable property for RotationY.
///
public static readonly BindableProperty RotationYProperty =
BindableProperty.Create(
nameof(RotationY),
typeof(double),
typeof(SkiaView),
0.0,
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
///
/// Bindable property for TranslationX.
///
public static readonly BindableProperty TranslationXProperty =
BindableProperty.Create(
nameof(TranslationX),
typeof(double),
typeof(SkiaView),
0.0,
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
///
/// Bindable property for TranslationY.
///
public static readonly BindableProperty TranslationYProperty =
BindableProperty.Create(
nameof(TranslationY),
typeof(double),
typeof(SkiaView),
0.0,
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
///
/// Bindable property for AnchorX.
///
public static readonly BindableProperty AnchorXProperty =
BindableProperty.Create(
nameof(AnchorX),
typeof(double),
typeof(SkiaView),
0.5,
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
///
/// Bindable property for AnchorY.
///
public static readonly BindableProperty AnchorYProperty =
BindableProperty.Create(
nameof(AnchorY),
typeof(double),
typeof(SkiaView),
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;
private Rect _bounds;
private SkiaView? _parent;
private readonly List _children = new();
///
/// Gets the absolute bounds of this view in screen coordinates.
///
public Rect GetAbsoluteBounds()
{
var bounds = Bounds;
var current = Parent;
while (current != null)
{
// Adjust for scroll offset if parent is a ScrollView
if (current is SkiaScrollView scrollView)
{
bounds = new Rect(
bounds.Left - scrollView.ScrollX,
bounds.Top - scrollView.ScrollY,
bounds.Width,
bounds.Height);
}
current = current.Parent;
}
return bounds;
}
///
/// Gets or sets the bounds of this view in parent coordinates.
/// Uses MAUI Rect for public API compliance.
///
public Rect Bounds
{
get => _bounds;
set
{
if (_bounds != value)
{
_bounds = value;
OnBoundsChanged();
}
}
}
///
/// Gets the bounds as SKRect for internal SkiaSharp rendering.
///
internal SKRect BoundsSK => new SKRect(
(float)_bounds.Left,
(float)_bounds.Top,
(float)_bounds.Right,
(float)_bounds.Bottom);
///
/// Gets or sets whether this view is visible.
///
public bool IsVisible
{
get => (bool)GetValue(IsVisibleProperty);
set => SetValue(IsVisibleProperty, value);
}
///
/// Gets or sets whether this view is enabled for interaction.
///
public bool IsEnabled
{
get => (bool)GetValue(IsEnabledProperty);
set => SetValue(IsEnabledProperty, value);
}
///
/// Gets or sets the opacity of this view (0.0 to 1.0).
///
public float Opacity
{
get => (float)GetValue(OpacityProperty);
set => SetValue(OpacityProperty, value);
}
///
/// Gets or sets the background color.
/// Uses Microsoft.Maui.Graphics.Color for MAUI compliance.
///
private SKColor _backgroundColorSK = SKColors.Transparent;
public Color? BackgroundColor
{
get => (Color?)GetValue(BackgroundColorProperty);
set => SetValue(BackgroundColorProperty, value);
}
///
/// Called when BackgroundColor changes.
///
private void OnBackgroundColorChanged()
{
var color = BackgroundColor;
_backgroundColorSK = color != null ? color.ToSKColor() : SKColors.Transparent;
Invalidate();
}
///
/// Gets the effective background color as SKColor for rendering.
///
protected SKColor GetEffectiveBackgroundColor() => _backgroundColorSK;
///
/// Gets or sets the requested width.
///
public double WidthRequest
{
get => (double)GetValue(WidthRequestProperty);
set => SetValue(WidthRequestProperty, value);
}
///
/// Gets or sets the requested height.
///
public double HeightRequest
{
get => (double)GetValue(HeightRequestProperty);
set => SetValue(HeightRequestProperty, value);
}
///
/// Gets or sets the minimum width request.
///
public double MinimumWidthRequest
{
get => (double)GetValue(MinimumWidthRequestProperty);
set => SetValue(MinimumWidthRequestProperty, value);
}
///
/// Gets or sets the minimum height request.
///
public double MinimumHeightRequest
{
get => (double)GetValue(MinimumHeightRequestProperty);
set => SetValue(MinimumHeightRequestProperty, value);
}
///
/// Gets or sets the requested width (backwards compatibility alias).
///
public double RequestedWidth
{
get => WidthRequest;
set => WidthRequest = value;
}
///
/// Gets or sets the requested height (backwards compatibility alias).
///
public double RequestedHeight
{
get => HeightRequest;
set => HeightRequest = value;
}
///
/// Gets or sets whether this view can receive keyboard focus.
///
public bool IsFocusable
{
get => (bool)GetValue(IsFocusableProperty);
set => SetValue(IsFocusableProperty, value);
}
///
/// Gets or sets the margin around this view.
///
public Thickness Margin
{
get => (Thickness)GetValue(MarginProperty);
set => SetValue(MarginProperty, value);
}
///
/// Gets or sets the horizontal layout options.
///
public LayoutOptions HorizontalOptions
{
get => (LayoutOptions)GetValue(HorizontalOptionsProperty);
set => SetValue(HorizontalOptionsProperty, value);
}
///
/// Gets or sets the vertical layout options.
///
public LayoutOptions VerticalOptions
{
get => (LayoutOptions)GetValue(VerticalOptionsProperty);
set => SetValue(VerticalOptionsProperty, value);
}
///
/// Gets or sets the name of this view (used for template child lookup).
///
public string Name
{
get => (string)GetValue(NameProperty);
set => SetValue(NameProperty, value);
}
///
/// Gets or sets the uniform scale factor.
///
public double Scale
{
get => (double)GetValue(ScaleProperty);
set => SetValue(ScaleProperty, value);
}
///
/// Gets or sets the X-axis scale factor.
///
public double ScaleX
{
get => (double)GetValue(ScaleXProperty);
set => SetValue(ScaleXProperty, value);
}
///
/// Gets or sets the Y-axis scale factor.
///
public double ScaleY
{
get => (double)GetValue(ScaleYProperty);
set => SetValue(ScaleYProperty, value);
}
///
/// Gets or sets the rotation in degrees around the Z-axis.
///
public double Rotation
{
get => (double)GetValue(RotationProperty);
set => SetValue(RotationProperty, value);
}
///
/// Gets or sets the rotation in degrees around the X-axis.
///
public double RotationX
{
get => (double)GetValue(RotationXProperty);
set => SetValue(RotationXProperty, value);
}
///
/// Gets or sets the rotation in degrees around the Y-axis.
///
public double RotationY
{
get => (double)GetValue(RotationYProperty);
set => SetValue(RotationYProperty, value);
}
///
/// Gets or sets the X translation offset.
///
public double TranslationX
{
get => (double)GetValue(TranslationXProperty);
set => SetValue(TranslationXProperty, value);
}
///
/// Gets or sets the Y translation offset.
///
public double TranslationY
{
get => (double)GetValue(TranslationYProperty);
set => SetValue(TranslationYProperty, value);
}
///
/// Gets or sets the X anchor point for transforms (0.0 to 1.0).
///
public double AnchorX
{
get => (double)GetValue(AnchorXProperty);
set => SetValue(AnchorXProperty, value);
}
///
/// Gets or sets the Y anchor point for transforms (0.0 to 1.0).
///
public double AnchorY
{
get => (double)GetValue(AnchorYProperty);
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.
///
public CursorType CursorType { get; set; }
///
/// Gets or sets the MAUI View this platform view represents.
/// Used for gesture processing.
///
public View? MauiView { get; set; }
///
/// Gets or sets whether this view currently has keyboard focus.
///
public bool IsFocused { get; internal set; }
///
/// Gets or sets the parent view.
///
public SkiaView? Parent
{
get => _parent;
internal set => _parent = value;
}
///
/// Gets the bounds of this view in screen coordinates (accounting for scroll offsets).
///
public Rect ScreenBounds
{
get
{
var bounds = Bounds;
var parent = _parent;
// Walk up the tree and adjust for scroll offsets
while (parent != null)
{
if (parent is SkiaScrollView scrollView)
{
bounds = new Rect(
bounds.Left - scrollView.ScrollX,
bounds.Top - scrollView.ScrollY,
bounds.Width,
bounds.Height);
}
parent = parent.Parent;
}
return bounds;
}
}
///
/// Gets the desired size calculated during measure.
/// Uses MAUI Size for public API compliance.
///
public Size DesiredSize { get; protected set; }
///
/// Gets the desired size as SKSize for internal SkiaSharp rendering.
///
internal SKSize DesiredSizeSK => new SKSize((float)DesiredSize.Width, (float)DesiredSize.Height);
///
/// Gets the child views.
///
public IReadOnlyList Children => _children;
///
/// Event raised when this view needs to be redrawn.
///
public event EventHandler? Invalidated;
///
/// Called when visibility changes.
///
protected virtual void OnVisibilityChanged()
{
Invalidate();
}
///
/// Called when enabled state changes.
///
protected virtual void OnEnabledChanged()
{
Invalidate();
}
///
/// Called when binding context changes. Propagates to children.
///
protected override void OnBindingContextChanged()
{
base.OnBindingContextChanged();
// Propagate binding context to children
foreach (var child in _children)
{
SetInheritedBindingContext(child, BindingContext);
}
}
///
/// Adds a child view.
///
public void AddChild(SkiaView child)
{
if (child._parent != null)
throw new InvalidOperationException("View already has a parent");
child._parent = this;
_children.Add(child);
// Propagate binding context to new child
if (BindingContext != null)
{
SetInheritedBindingContext(child, BindingContext);
}
Invalidate();
}
///
/// Removes a child view.
///
public void RemoveChild(SkiaView child)
{
if (child._parent != this)
return;
child._parent = null;
_children.Remove(child);
Invalidate();
}
///
/// Inserts a child view at the specified index.
///
public void InsertChild(int index, SkiaView child)
{
if (child._parent != null)
throw new InvalidOperationException("View already has a parent");
child._parent = this;
_children.Insert(index, child);
// Propagate binding context to new child
if (BindingContext != null)
{
SetInheritedBindingContext(child, BindingContext);
}
Invalidate();
}
///
/// Removes all child views.
///
public void ClearChildren()
{
foreach (var child in _children)
{
child._parent = null;
}
_children.Clear();
Invalidate();
}
///
/// Requests that this view be redrawn.
/// Thread-safe - will marshal to GTK thread if needed.
///
public void Invalidate()
{
// Check if we're on the GTK thread - if not, marshal the entire call
int currentThread = Environment.CurrentManagedThreadId;
int gtkThread = LinuxApplication.GtkThreadId;
if (gtkThread != 0 && currentThread != gtkThread)
{
GLibNative.IdleAdd(() =>
{
InvalidateInternal();
return false;
});
return;
}
InvalidateInternal();
}
private void InvalidateInternal()
{
LinuxApplication.LogInvalidate(GetType().Name);
Invalidated?.Invoke(this, EventArgs.Empty);
// Notify rendering engine of dirty region
if (Bounds.Width > 0 && Bounds.Height > 0)
{
SkiaRenderingEngine.Current?.InvalidateRegion(new SKRect(
(float)Bounds.Left, (float)Bounds.Top,
(float)Bounds.Right, (float)Bounds.Bottom));
}
if (_parent != null)
{
_parent.InvalidateInternal();
}
else
{
LinuxApplication.RequestRedraw();
}
}
///
/// Invalidates the cached measurement.
///
public void InvalidateMeasure()
{
DesiredSize = Size.Zero;
_parent?.InvalidateMeasure();
Invalidate();
}
///
/// Called when the bounds change.
///
protected virtual void OnBoundsChanged()
{
Invalidate();
}
///
/// Measures the desired size of this view.
/// Uses MAUI Size for public API compliance.
///
public Size Measure(Size availableSize)
{
DesiredSize = MeasureOverride(availableSize);
return DesiredSize;
}
///
/// Override to provide custom measurement.
/// Uses MAUI Size for public API compliance.
///
protected virtual Size MeasureOverride(Size availableSize)
{
var width = WidthRequest >= 0 ? WidthRequest : 0;
var height = HeightRequest >= 0 ? HeightRequest : 0;
return new Size(width, height);
}
///
/// Arranges this view within the given bounds.
/// Uses MAUI Rect for public API compliance.
///
public virtual void Arrange(Rect bounds)
{
Bounds = ArrangeOverride(bounds);
}
///
/// Override to customize arrangement within the given bounds.
/// Uses MAUI Rect for public API compliance.
///
protected virtual Rect ArrangeOverride(Rect bounds)
{
return bounds;
}
///
/// Performs hit testing to find the view at the given point.
/// Uses MAUI Point for public API compliance.
///
public virtual SkiaView? HitTest(Point point)
{
return HitTest((float)point.X, (float)point.Y);
}
///
/// Performs hit testing to find the view at the given coordinates.
/// Coordinates are in absolute window space, matching how Bounds are stored.
///
public virtual SkiaView? HitTest(float x, float y)
{
if (!IsVisible || !IsEnabled)
return null;
if (!Bounds.Contains(x, y))
return null;
// Check children in reverse order (top-most first)
// Coordinates stay in absolute space since children have absolute Bounds
for (int i = _children.Count - 1; i >= 0; i--)
{
var hit = _children[i].HitTest(x, y);
if (hit != null)
return hit;
}
// If InputTransparent, don't capture input - let it pass through
if (InputTransparent)
return null;
return this;
}
}
///
/// Event args for pointer events.
///
public class PointerEventArgs : EventArgs
{
public float X { get; }
public float Y { get; }
public PointerButton Button { get; }
public bool Handled { get; set; }
public PointerEventArgs(float x, float y, PointerButton button = PointerButton.None)
{
X = x;
Y = y;
Button = button;
}
}
///
/// Mouse button flags.
///
[Flags]
public enum PointerButton
{
None = 0,
Left = 1,
Middle = 2,
Right = 4,
XButton1 = 8,
XButton2 = 16
}
///
/// Event args for scroll events.
///
public class ScrollEventArgs : EventArgs
{
public float X { get; }
public float Y { get; }
public float DeltaX { get; }
public float DeltaY { get; }
public KeyModifiers Modifiers { get; }
public bool Handled { get; set; }
///
/// Gets whether the Control key is pressed during this scroll event.
///
public bool IsControlPressed => (Modifiers & KeyModifiers.Control) != 0;
public ScrollEventArgs(float x, float y, float deltaX, float deltaY, KeyModifiers modifiers = KeyModifiers.None)
{
X = x;
Y = y;
DeltaX = deltaX;
DeltaY = deltaY;
Modifiers = modifiers;
}
}
///
/// Event args for keyboard events.
///
public class KeyEventArgs : EventArgs
{
public Key Key { get; }
public KeyModifiers Modifiers { get; }
public bool Handled { get; set; }
public KeyEventArgs(Key key, KeyModifiers modifiers = KeyModifiers.None)
{
Key = key;
Modifiers = modifiers;
}
}
///
/// Event args for text input events.
///
public class TextInputEventArgs : EventArgs
{
public string Text { get; }
public bool Handled { get; set; }
public TextInputEventArgs(string text)
{
Text = text;
}
}
///
/// Keyboard modifier flags.
///
[Flags]
public enum KeyModifiers
{
None = 0,
Shift = 1,
Control = 2,
Alt = 4,
Super = 8,
CapsLock = 16,
NumLock = 32
}