// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using SkiaSharp; namespace Microsoft.Maui.Platform; /// /// Base class for all Skia-rendered views on Linux. /// public abstract class SkiaView : IDisposable { // Popup overlay system for dropdowns, calendars, etc. private static readonly List<(SkiaView Owner, Action Draw)> _popupOverlays = new(); public static void RegisterPopupOverlay(SkiaView owner, Action drawAction) { _popupOverlays.RemoveAll(p => p.Owner == owner); _popupOverlays.Add((owner, drawAction)); } public static void UnregisterPopupOverlay(SkiaView owner) { _popupOverlays.RemoveAll(p => p.Owner == owner); } public static void DrawPopupOverlays(SKCanvas canvas) { // Restore canvas to clean state for overlay drawing // Save count tells us how many unmatched Saves there are while (canvas.SaveCount > 1) { canvas.Restore(); } foreach (var (_, draw) in _popupOverlays) { canvas.Save(); draw(canvas); canvas.Restore(); } } /// /// Gets the absolute bounds of this view in screen coordinates. /// public SKRect 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 SKRect( bounds.Left - scrollView.ScrollX, bounds.Top - scrollView.ScrollY, bounds.Right - scrollView.ScrollX, bounds.Bottom - scrollView.ScrollY); } current = current.Parent; } return bounds; } private bool _disposed; private SKRect _bounds; private bool _isVisible = true; private bool _isEnabled = true; private float _opacity = 1.0f; private SKColor _backgroundColor = SKColors.Transparent; private SkiaView? _parent; private readonly List _children = new(); /// /// Gets or sets the bounds of this view in parent coordinates. /// public SKRect Bounds { get => _bounds; set { if (_bounds != value) { _bounds = value; OnBoundsChanged(); } } } /// /// Gets or sets whether this view is visible. /// public bool IsVisible { get => _isVisible; set { if (_isVisible != value) { _isVisible = value; Invalidate(); } } } /// /// Gets or sets whether this view is enabled for interaction. /// public bool IsEnabled { get => _isEnabled; set { if (_isEnabled != value) { _isEnabled = value; Invalidate(); } } } /// /// Gets or sets the opacity of this view (0.0 to 1.0). /// public float Opacity { get => _opacity; set { var clamped = Math.Clamp(value, 0f, 1f); if (_opacity != clamped) { _opacity = clamped; Invalidate(); } } } /// /// Gets or sets the background color. /// public SKColor BackgroundColor { get => _backgroundColor; set { if (_backgroundColor != value) { _backgroundColor = value; Invalidate(); } } } /// /// Gets or sets the requested width. /// public double RequestedWidth { get; set; } = -1; /// /// Gets or sets the requested height. /// public double RequestedHeight { get; set; } = -1; /// /// Gets or sets whether this view can receive keyboard focus. /// public bool IsFocusable { 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 desired size calculated during measure. /// public SKSize DesiredSize { get; protected set; } /// /// Gets the child views. /// public IReadOnlyList Children => _children; /// /// Event raised when this view needs to be redrawn. /// public event EventHandler? Invalidated; /// /// 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); 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); 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. /// public void Invalidate() { Invalidated?.Invoke(this, EventArgs.Empty); _parent?.Invalidate(); } /// /// Invalidates the cached measurement. /// public void InvalidateMeasure() { DesiredSize = SKSize.Empty; _parent?.InvalidateMeasure(); Invalidate(); } /// /// Draws this view and its children to the canvas. /// public void Draw(SKCanvas canvas) { if (!IsVisible || Opacity <= 0) return; canvas.Save(); // Apply opacity if (Opacity < 1.0f) { canvas.SaveLayer(new SKPaint { Color = SKColors.White.WithAlpha((byte)(Opacity * 255)) }); } // Draw background at absolute bounds if (BackgroundColor != SKColors.Transparent) { using var paint = new SKPaint { Color = BackgroundColor }; canvas.DrawRect(Bounds, paint); } // Draw content at absolute bounds OnDraw(canvas, Bounds); // Draw children - they draw at their own absolute bounds foreach (var child in _children) { child.Draw(canvas); } if (Opacity < 1.0f) { canvas.Restore(); } canvas.Restore(); } /// /// Override to draw custom content. /// protected virtual void OnDraw(SKCanvas canvas, SKRect bounds) { } /// /// Called when the bounds change. /// protected virtual void OnBoundsChanged() { Invalidate(); } /// /// Measures the desired size of this view. /// public SKSize Measure(SKSize availableSize) { DesiredSize = MeasureOverride(availableSize); return DesiredSize; } /// /// Override to provide custom measurement. /// protected virtual SKSize MeasureOverride(SKSize availableSize) { var width = RequestedWidth >= 0 ? (float)RequestedWidth : 0; var height = RequestedHeight >= 0 ? (float)RequestedHeight : 0; return new SKSize(width, height); } /// /// Arranges this view within the given bounds. /// public void Arrange(SKRect bounds) { Bounds = ArrangeOverride(bounds); } /// /// Override to customize arrangement within the given bounds. /// protected virtual SKRect ArrangeOverride(SKRect bounds) { return bounds; } /// /// Performs hit testing to find the view at the given point. /// public virtual SkiaView? HitTest(SKPoint point) { return HitTest(point.X, point.Y); } /// /// Performs hit testing to find the view at the given coordinates. /// 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) var localX = x - Bounds.Left; var localY = y - Bounds.Top; for (int i = _children.Count - 1; i >= 0; i--) { var hit = _children[i].HitTest(localX, localY); if (hit != null) return hit; } return this; } #region Input Events public virtual void OnPointerEntered(PointerEventArgs e) { } public virtual void OnPointerExited(PointerEventArgs e) { } public virtual void OnPointerMoved(PointerEventArgs e) { } public virtual void OnPointerPressed(PointerEventArgs e) { } public virtual void OnPointerReleased(PointerEventArgs e) { } public virtual void OnScroll(ScrollEventArgs e) { } public virtual void OnKeyDown(KeyEventArgs e) { } public virtual void OnKeyUp(KeyEventArgs e) { } public virtual void OnTextInput(TextInputEventArgs e) { } public virtual void OnFocusGained() { IsFocused = true; Invalidate(); } public virtual void OnFocusLost() { IsFocused = false; Invalidate(); } #endregion #region IDisposable protected virtual void Dispose(bool disposing) { if (!_disposed) { if (disposing) { foreach (var child in _children) { child.Dispose(); } _children.Clear(); } _disposed = true; } } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } #endregion } /// /// 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 bool Handled { get; set; } public ScrollEventArgs(float x, float y, float deltaX, float deltaY) { X = x; Y = y; DeltaX = deltaX; DeltaY = deltaY; } } /// /// 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 }