Files

312 lines
9.6 KiB
C#
Raw Permalink Normal View History

// 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<SKCanvas> Draw)> _popupOverlays = new();
public static void RegisterPopupOverlay(SkiaView owner, Action<SKCanvas> 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();
}
}
/// <summary>
/// Gets the popup owner that should receive pointer events at the given coordinates.
/// This allows popups to receive events even outside their normal bounds.
/// </summary>
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;
}
/// <summary>
/// Checks if there are any active popup overlays.
/// </summary>
public static bool HasActivePopup => _popupOverlays.Count > 0;
/// <summary>
/// Override this to define the popup area for hit testing.
/// </summary>
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;
/// <summary>
/// Gets whether high contrast mode is enabled.
/// </summary>
public static bool IsHighContrastEnabled => _highContrastService?.IsHighContrastEnabled ?? false;
/// <summary>
/// Gets the current high contrast colors, or default colors if not in high contrast mode.
/// </summary>
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<IAccessible>? _accessibleChildren;
/// <summary>
/// Gets or sets the accessibility name for screen readers.
/// </summary>
public string? SemanticName { get; set; }
/// <summary>
/// Gets or sets the accessibility description for screen readers.
/// </summary>
public string? SemanticDescription { get; set; }
/// <summary>
/// Gets or sets the accessibility hint for screen readers.
/// </summary>
public string? SemanticHint { get; set; }
/// <summary>
/// Gets the accessibility service instance.
/// </summary>
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
}
}
/// <summary>
/// Registers this view with the accessibility service.
/// </summary>
protected void RegisterAccessibility()
{
AccessibilityService?.Register(this);
}
/// <summary>
/// Unregisters this view from the accessibility service.
/// </summary>
protected void UnregisterAccessibility()
{
AccessibilityService?.Unregister(this);
}
/// <summary>
/// Announces text to screen readers.
/// </summary>
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> IAccessible.Children => _accessibleChildren ??= GetAccessibleChildren();
AccessibleRect IAccessible.Bounds => new AccessibleRect(
(int)ScreenBounds.Left,
(int)ScreenBounds.Top,
(int)ScreenBounds.Width,
(int)ScreenBounds.Height);
IReadOnlyList<AccessibleAction> 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);
/// <summary>
/// Gets the default accessible name based on view content.
/// </summary>
protected virtual string GetDefaultAccessibleName() => string.Empty;
/// <summary>
/// Gets the accessible role for this view.
/// </summary>
protected virtual AccessibleRole GetAccessibleRole() => AccessibleRole.Unknown;
/// <summary>
/// Gets the current accessible states.
/// </summary>
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;
}
/// <summary>
/// Gets the accessible children of this view.
/// </summary>
protected virtual List<IAccessible> GetAccessibleChildren()
{
var children = new List<IAccessible>();
foreach (var child in Children)
{
if (child is IAccessible accessible)
{
children.Add(accessible);
}
}
return children;
}
/// <summary>
/// Gets the available accessible actions.
/// </summary>
protected virtual IReadOnlyList<AccessibleAction> GetAccessibleActions()
{
return Array.Empty<AccessibleAction>();
}
/// <summary>
/// Performs an accessible action.
/// </summary>
protected virtual bool DoAccessibleAction(string actionName) => false;
/// <summary>
/// Gets the accessible value (for sliders, progress bars, etc.).
/// </summary>
protected virtual double? GetAccessibleValue() => null;
/// <summary>
/// Gets the minimum accessible value.
/// </summary>
protected virtual double? GetAccessibleMinValue() => null;
/// <summary>
/// Gets the maximum accessible value.
/// </summary>
protected virtual double? GetAccessibleMaxValue() => null;
/// <summary>
/// Sets the accessible value.
/// </summary>
protected virtual bool SetAccessibleValue(double value) => false;
#endregion
}