// 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.Graphics; using Microsoft.Maui.Platform.Linux.Services; using SkiaSharp; using Microsoft.Maui; namespace Microsoft.Maui.Platform; /// /// Base class for layout containers that can arrange child views. /// public abstract class SkiaLayoutView : SkiaView { #region BindableProperties /// /// Bindable property for Spacing. /// public static readonly BindableProperty SpacingProperty = BindableProperty.Create( nameof(Spacing), typeof(double), typeof(SkiaLayoutView), 0.0, BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaLayoutView)b).InvalidateMeasure()); /// /// Bindable property for Padding. /// public static readonly BindableProperty PaddingProperty = BindableProperty.Create( nameof(Padding), typeof(Thickness), typeof(SkiaLayoutView), default(Thickness), BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaLayoutView)b).InvalidateMeasure()); /// /// Bindable property for ClipToBounds. /// public static readonly BindableProperty ClipToBoundsProperty = BindableProperty.Create( nameof(ClipToBounds), typeof(bool), typeof(SkiaLayoutView), false, BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaLayoutView)b).Invalidate()); #endregion private readonly List _children = new(); /// /// Gets the children of this layout. /// public new IReadOnlyList Children => _children; /// /// Spacing between children. /// public double Spacing { get => (double)GetValue(SpacingProperty); set => SetValue(SpacingProperty, value); } /// /// Padding around the content. /// public Thickness Padding { get => (Thickness)GetValue(PaddingProperty); set => SetValue(PaddingProperty, value); } /// /// Gets or sets whether child views are clipped to the bounds. /// public bool ClipToBounds { get => (bool)GetValue(ClipToBoundsProperty); set => SetValue(ClipToBoundsProperty, value); } /// /// Called when binding context changes. Propagates to layout children. /// protected override void OnBindingContextChanged() { base.OnBindingContextChanged(); // Propagate binding context to layout children foreach (var child in _children) { SetInheritedBindingContext(child, BindingContext); } } /// /// Adds a child view. /// public virtual void AddChild(SkiaView child) { if (child.Parent != null) { throw new InvalidOperationException("View already has a parent"); } _children.Add(child); child.Parent = this; // Propagate binding context to new child if (BindingContext != null) { SetInheritedBindingContext(child, BindingContext); } InvalidateMeasure(); Invalidate(); } /// /// Removes a child view. /// public virtual void RemoveChild(SkiaView child) { if (_children.Remove(child)) { child.Parent = null; InvalidateMeasure(); Invalidate(); } } /// /// Removes a child at the specified index. /// public virtual void RemoveChildAt(int index) { if (index >= 0 && index < _children.Count) { var child = _children[index]; _children.RemoveAt(index); child.Parent = null; InvalidateMeasure(); Invalidate(); } } /// /// Inserts a child at the specified index. /// public virtual void InsertChild(int index, SkiaView child) { if (child.Parent != null) { throw new InvalidOperationException("View already has a parent"); } _children.Insert(index, child); child.Parent = this; // Propagate binding context to new child if (BindingContext != null) { SetInheritedBindingContext(child, BindingContext); } InvalidateMeasure(); Invalidate(); } /// /// Clears all children. /// public virtual void ClearChildren() { foreach (var child in _children) { child.Parent = null; } _children.Clear(); InvalidateMeasure(); Invalidate(); } /// /// Gets the content bounds (bounds minus padding). /// protected virtual SKRect GetContentBounds() { return GetContentBounds(new SKRect((float)Bounds.Left, (float)Bounds.Top, (float)(Bounds.Left + Bounds.Width), (float)(Bounds.Top + Bounds.Height))); } /// /// Gets the content bounds for a given bounds rectangle. /// protected SKRect GetContentBounds(SKRect bounds) { return new SKRect( bounds.Left + (float)Padding.Left, bounds.Top + (float)Padding.Top, bounds.Right - (float)Padding.Right, bounds.Bottom - (float)Padding.Bottom); } protected override void OnDraw(SKCanvas canvas, SKRect bounds) { // Draw background if set (for layouts inside CollectionView items) if (BackgroundColor != null && BackgroundColor != Colors.Transparent) { using var bgPaint = new SKPaint { Color = GetEffectiveBackgroundColor(), Style = SKPaintStyle.Fill }; canvas.DrawRect(bounds, bgPaint); } // Log for StackLayout if (this is SkiaStackLayout) { bool hasCV = false; foreach (var c in _children) { if (c is SkiaCollectionView) hasCV = true; } if (hasCV) { DiagnosticLog.Debug("SkiaLayoutView", $"[SkiaStackLayout+CV] OnDraw - bounds={bounds}, children={_children.Count}"); foreach (var c in _children) { DiagnosticLog.Debug("SkiaLayoutView", $"[SkiaStackLayout+CV] Child: {c.GetType().Name}, IsVisible={c.IsVisible}, Bounds={c.Bounds}"); } } } // Draw children in order foreach (var child in _children) { if (child.IsVisible) { child.Draw(canvas); } } } public override SkiaView? HitTest(float x, float y) { if (!IsVisible || !IsEnabled || !Bounds.Contains(x, y)) return null; // Hit test children in reverse order (top-most first) for (int i = _children.Count - 1; i >= 0; i--) { var child = _children[i]; var hit = child.HitTest(x, y); if (hit != null) return hit; } return this; } /// /// Forward pointer pressed events to the appropriate child. /// public override void OnPointerPressed(PointerEventArgs e) { // Find which child was hit and forward the event var hit = HitTest(e.X, e.Y); if (hit != null && hit != this) { hit.OnPointerPressed(e); } } /// /// Forward pointer released events to the appropriate child. /// public override void OnPointerReleased(PointerEventArgs e) { // Find which child was hit and forward the event var hit = HitTest(e.X, e.Y); if (hit != null && hit != this) { hit.OnPointerReleased(e); } } /// /// Forward pointer moved events to the appropriate child. /// public override void OnPointerMoved(PointerEventArgs e) { // Find which child was hit and forward the event var hit = HitTest(e.X, e.Y); if (hit != null && hit != this) { hit.OnPointerMoved(e); } } /// /// Forward scroll events to the appropriate child. /// public override void OnScroll(ScrollEventArgs e) { // Find which child was hit and forward the event var hit = HitTest(e.X, e.Y); if (hit != null && hit != this) { hit.OnScroll(e); } } }