// 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.Platform.Linux.Rendering; using Microsoft.Maui.Platform.Linux.Services; using SkiaSharp; namespace Microsoft.Maui.Platform; public abstract partial class SkiaView { // 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 popup owner that should receive pointer events at the given coordinates. /// This allows popups to receive events even outside their normal bounds. /// public static SkiaView? GetPopupOwnerAt(float x, float y) { // Check in reverse order (topmost popup first) for (int i = _popupOverlays.Count - 1; i >= 0; i--) { var owner = _popupOverlays[i].Owner; if (owner.HitTestPopupArea(x, y)) { return owner; } } return null; } /// /// Checks if there are any active popup overlays. /// public static bool HasActivePopup => _popupOverlays.Count > 0; /// /// Override this to define the popup area for hit testing. /// protected virtual bool HitTestPopupArea(float x, float y) { // Default: no popup area beyond normal bounds return Bounds.Contains(x, y); } #region High Contrast Support private static HighContrastService? _highContrastService; private static bool _highContrastInitialized; /// /// Gets whether high contrast mode is enabled. /// public static bool IsHighContrastEnabled => _highContrastService?.IsHighContrastEnabled ?? false; /// /// Gets the current high contrast colors, or default colors if not in high contrast mode. /// public static HighContrastColors GetHighContrastColors() { InitializeHighContrastService(); return _highContrastService?.GetColors() ?? new HighContrastColors { Background = SkiaTheme.BackgroundWhiteSK, Foreground = SkiaTheme.TextPrimarySK, Accent = SkiaTheme.PrimarySK, Border = SkiaTheme.BorderMediumSK, Error = SkiaTheme.ErrorSK, Success = SkiaTheme.SuccessSK, Warning = SkiaTheme.WarningSK, Link = SkiaTheme.TextLinkSK, LinkVisited = SkiaTheme.TextLinkVisitedSK, Selection = SkiaTheme.PrimarySK, SelectionText = SkiaTheme.BackgroundWhiteSK, DisabledText = SkiaTheme.TextDisabledSK, DisabledBackground = SkiaTheme.BackgroundDisabledSK }; } private static void InitializeHighContrastService() { if (_highContrastInitialized) return; _highContrastInitialized = true; try { _highContrastService = new HighContrastService(); _highContrastService.HighContrastChanged += OnHighContrastChanged; _highContrastService.Initialize(); } catch { // Ignore errors - high contrast is optional } } private static void OnHighContrastChanged(object? sender, HighContrastChangedEventArgs e) { // Request a full repaint of the UI SkiaRenderingEngine.Current?.InvalidateAll(); } #endregion #region Accessibility Support (IAccessible) private static IAccessibilityService? _accessibilityService; private static bool _accessibilityInitialized; private string _accessibleId = Guid.NewGuid().ToString(); private List? _accessibleChildren; /// /// Gets or sets the accessibility name for screen readers. /// public string? SemanticName { get; set; } /// /// Gets or sets the accessibility description for screen readers. /// public string? SemanticDescription { get; set; } /// /// Gets or sets the accessibility hint for screen readers. /// public string? SemanticHint { get; set; } /// /// Gets the accessibility service instance. /// protected static IAccessibilityService? AccessibilityService { get { InitializeAccessibilityService(); return _accessibilityService; } } private static void InitializeAccessibilityService() { if (_accessibilityInitialized) return; _accessibilityInitialized = true; try { _accessibilityService = AccessibilityServiceFactory.Instance; _accessibilityService?.Initialize(); } catch { // Ignore errors - accessibility is optional } } /// /// Registers this view with the accessibility service. /// protected void RegisterAccessibility() { AccessibilityService?.Register(this); } /// /// Unregisters this view from the accessibility service. /// protected void UnregisterAccessibility() { AccessibilityService?.Unregister(this); } /// /// Announces text to screen readers. /// protected void AnnounceToScreenReader(string text, AnnouncementPriority priority = AnnouncementPriority.Polite) { AccessibilityService?.Announce(text, priority); } // IAccessible implementation string IAccessible.AccessibleId => _accessibleId; string IAccessible.AccessibleName => SemanticName ?? GetDefaultAccessibleName(); string IAccessible.AccessibleDescription => SemanticDescription ?? SemanticHint ?? string.Empty; AccessibleRole IAccessible.Role => GetAccessibleRole(); AccessibleStates IAccessible.States => GetAccessibleStates(); IAccessible? IAccessible.Parent => Parent as IAccessible; IReadOnlyList IAccessible.Children => _accessibleChildren ??= GetAccessibleChildren(); AccessibleRect IAccessible.Bounds => new AccessibleRect( (int)ScreenBounds.Left, (int)ScreenBounds.Top, (int)ScreenBounds.Width, (int)ScreenBounds.Height); IReadOnlyList IAccessible.Actions => GetAccessibleActions(); double? IAccessible.Value => GetAccessibleValue(); double? IAccessible.MinValue => GetAccessibleMinValue(); double? IAccessible.MaxValue => GetAccessibleMaxValue(); bool IAccessible.DoAction(string actionName) => DoAccessibleAction(actionName); bool IAccessible.SetValue(double value) => SetAccessibleValue(value); /// /// Gets the default accessible name based on view content. /// protected virtual string GetDefaultAccessibleName() => string.Empty; /// /// Gets the accessible role for this view. /// protected virtual AccessibleRole GetAccessibleRole() => AccessibleRole.Unknown; /// /// Gets the current accessible states. /// protected virtual AccessibleStates GetAccessibleStates() { var states = AccessibleStates.None; if (IsVisible) states |= AccessibleStates.Visible; if (IsEnabled) states |= AccessibleStates.Enabled; if (IsFocused) states |= AccessibleStates.Focused; if (IsFocusable) states |= AccessibleStates.Focusable; return states; } /// /// Gets the accessible children of this view. /// protected virtual List GetAccessibleChildren() { var children = new List(); foreach (var child in Children) { if (child is IAccessible accessible) { children.Add(accessible); } } return children; } /// /// Gets the available accessible actions. /// protected virtual IReadOnlyList GetAccessibleActions() { return Array.Empty(); } /// /// Performs an accessible action. /// protected virtual bool DoAccessibleAction(string actionName) => false; /// /// Gets the accessible value (for sliders, progress bars, etc.). /// protected virtual double? GetAccessibleValue() => null; /// /// Gets the minimum accessible value. /// protected virtual double? GetAccessibleMinValue() => null; /// /// Gets the maximum accessible value. /// protected virtual double? GetAccessibleMaxValue() => null; /// /// Sets the accessible value. /// protected virtual bool SetAccessibleValue(double value) => false; #endregion }