// 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.Linux.Services;
///
/// Provides high contrast mode detection and theme support for accessibility.
///
public class HighContrastService
{
private bool _isHighContrastEnabled;
private HighContrastTheme _currentTheme = HighContrastTheme.None;
private bool _initialized;
///
/// Gets whether high contrast mode is enabled.
///
public bool IsHighContrastEnabled => _isHighContrastEnabled;
///
/// Gets the current high contrast theme.
///
public HighContrastTheme CurrentTheme => _currentTheme;
///
/// Event raised when high contrast mode changes.
///
public event EventHandler? HighContrastChanged;
///
/// Initializes the high contrast service.
///
public void Initialize()
{
if (_initialized) return;
_initialized = true;
DetectHighContrast();
}
///
/// Detects current high contrast mode settings.
///
public void DetectHighContrast()
{
bool isEnabled = false;
var theme = HighContrastTheme.None;
// Try GNOME settings
if (TryGetGnomeHighContrast(out bool gnomeEnabled, out string? gnomeTheme))
{
isEnabled = gnomeEnabled;
if (gnomeEnabled)
{
theme = ParseThemeName(gnomeTheme);
}
}
// Try KDE settings
else if (TryGetKdeHighContrast(out bool kdeEnabled, out string? kdeTheme))
{
isEnabled = kdeEnabled;
if (kdeEnabled)
{
theme = ParseThemeName(kdeTheme);
}
}
// Try GTK settings
else if (TryGetGtkHighContrast(out bool gtkEnabled, out string? gtkTheme))
{
isEnabled = gtkEnabled;
if (gtkEnabled)
{
theme = ParseThemeName(gtkTheme);
}
}
// Check environment variables
else if (TryGetEnvironmentHighContrast(out bool envEnabled))
{
isEnabled = envEnabled;
theme = HighContrastTheme.WhiteOnBlack; // Default
}
UpdateHighContrast(isEnabled, theme);
}
private void UpdateHighContrast(bool isEnabled, HighContrastTheme theme)
{
if (_isHighContrastEnabled != isEnabled || _currentTheme != theme)
{
_isHighContrastEnabled = isEnabled;
_currentTheme = theme;
HighContrastChanged?.Invoke(this, new HighContrastChangedEventArgs(isEnabled, theme));
}
}
private static bool TryGetGnomeHighContrast(out bool isEnabled, out string? themeName)
{
isEnabled = false;
themeName = null;
try
{
// Check if high contrast is enabled via gsettings
var result = RunCommand("gsettings", "get org.gnome.desktop.a11y.interface high-contrast");
if (!string.IsNullOrEmpty(result))
{
isEnabled = result.Trim().ToLower() == "true";
}
// Get the current GTK theme
result = RunCommand("gsettings", "get org.gnome.desktop.interface gtk-theme");
if (!string.IsNullOrEmpty(result))
{
themeName = result.Trim().Trim('\'');
// Check if theme name indicates high contrast
if (!isEnabled && themeName != null)
{
var lowerTheme = themeName.ToLower();
isEnabled = lowerTheme.Contains("highcontrast") ||
lowerTheme.Contains("high-contrast") ||
lowerTheme.Contains("hc");
}
}
return true;
}
catch
{
return false;
}
}
private static bool TryGetKdeHighContrast(out bool isEnabled, out string? themeName)
{
isEnabled = false;
themeName = null;
try
{
// Check kdeglobals for color scheme
var configPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".config", "kdeglobals");
if (!File.Exists(configPath)) return false;
var lines = File.ReadAllLines(configPath);
foreach (var line in lines)
{
if (line.StartsWith("ColorScheme="))
{
themeName = line.Substring("ColorScheme=".Length);
var lowerTheme = themeName.ToLower();
isEnabled = lowerTheme.Contains("highcontrast") ||
lowerTheme.Contains("high-contrast") ||
lowerTheme.Contains("breeze-high-contrast");
return true;
}
}
return false;
}
catch
{
return false;
}
}
private static bool TryGetGtkHighContrast(out bool isEnabled, out string? themeName)
{
isEnabled = false;
themeName = null;
try
{
// Check GTK settings.ini
var gtkConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".config", "gtk-3.0", "settings.ini");
if (!File.Exists(gtkConfigPath))
{
gtkConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".config", "gtk-4.0", "settings.ini");
}
if (!File.Exists(gtkConfigPath)) return false;
var lines = File.ReadAllLines(gtkConfigPath);
foreach (var line in lines)
{
if (line.StartsWith("gtk-theme-name="))
{
themeName = line.Substring("gtk-theme-name=".Length);
var lowerTheme = themeName.ToLower();
isEnabled = lowerTheme.Contains("highcontrast") ||
lowerTheme.Contains("high-contrast");
return true;
}
}
return false;
}
catch
{
return false;
}
}
private static bool TryGetEnvironmentHighContrast(out bool isEnabled)
{
isEnabled = false;
// Check GTK_THEME environment variable
var gtkTheme = Environment.GetEnvironmentVariable("GTK_THEME");
if (!string.IsNullOrEmpty(gtkTheme))
{
var lower = gtkTheme.ToLower();
isEnabled = lower.Contains("highcontrast") || lower.Contains("high-contrast");
if (isEnabled) return true;
}
// Check accessibility-related env vars
var forceA11y = Environment.GetEnvironmentVariable("GTK_A11Y");
if (forceA11y?.ToLower() == "atspi" || forceA11y == "1")
{
// A11y is forced, but doesn't necessarily mean high contrast
}
return isEnabled;
}
private static HighContrastTheme ParseThemeName(string? themeName)
{
if (string.IsNullOrEmpty(themeName))
return HighContrastTheme.WhiteOnBlack;
var lower = themeName.ToLower();
if (lower.Contains("inverse") || lower.Contains("dark") || lower.Contains("white-on-black"))
return HighContrastTheme.WhiteOnBlack;
if (lower.Contains("light") || lower.Contains("black-on-white"))
return HighContrastTheme.BlackOnWhite;
// Default to white on black (more common high contrast choice)
return HighContrastTheme.WhiteOnBlack;
}
///
/// Gets the appropriate colors for the current high contrast theme.
///
public HighContrastColors GetColors()
{
return _currentTheme switch
{
HighContrastTheme.WhiteOnBlack => new HighContrastColors
{
Background = SKColors.Black,
Foreground = SKColors.White,
Accent = new SKColor(0, 255, 255), // Cyan
Border = SKColors.White,
Error = new SKColor(255, 100, 100),
Success = new SKColor(100, 255, 100),
Warning = SKColors.Yellow,
Link = new SKColor(100, 200, 255),
LinkVisited = new SKColor(200, 150, 255),
Selection = new SKColor(0, 120, 215),
SelectionText = SKColors.White,
DisabledText = new SKColor(160, 160, 160),
DisabledBackground = new SKColor(40, 40, 40)
},
HighContrastTheme.BlackOnWhite => new HighContrastColors
{
Background = SKColors.White,
Foreground = SKColors.Black,
Accent = new SKColor(0, 0, 200), // Dark blue
Border = SKColors.Black,
Error = new SKColor(180, 0, 0),
Success = new SKColor(0, 130, 0),
Warning = new SKColor(180, 120, 0),
Link = new SKColor(0, 0, 180),
LinkVisited = new SKColor(80, 0, 120),
Selection = new SKColor(0, 120, 215),
SelectionText = SKColors.White,
DisabledText = new SKColor(100, 100, 100),
DisabledBackground = new SKColor(220, 220, 220)
},
_ => GetDefaultColors()
};
}
private static HighContrastColors GetDefaultColors()
{
return new HighContrastColors
{
Background = SKColors.White,
Foreground = new SKColor(33, 33, 33),
Accent = new SKColor(33, 150, 243),
Border = new SKColor(200, 200, 200),
Error = new SKColor(244, 67, 54),
Success = new SKColor(76, 175, 80),
Warning = new SKColor(255, 152, 0),
Link = new SKColor(33, 150, 243),
LinkVisited = new SKColor(156, 39, 176),
Selection = new SKColor(33, 150, 243),
SelectionText = SKColors.White,
DisabledText = new SKColor(158, 158, 158),
DisabledBackground = new SKColor(238, 238, 238)
};
}
///
/// Forces a specific high contrast mode (for testing or user preference override).
///
public void ForceHighContrast(bool enabled, HighContrastTheme theme = HighContrastTheme.WhiteOnBlack)
{
UpdateHighContrast(enabled, theme);
}
private static string? RunCommand(string command, string arguments)
{
try
{
using var process = new System.Diagnostics.Process();
process.StartInfo = new System.Diagnostics.ProcessStartInfo
{
FileName = command,
Arguments = arguments,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
process.Start();
var output = process.StandardOutput.ReadToEnd();
process.WaitForExit(1000);
return output;
}
catch
{
return null;
}
}
}
///
/// High contrast theme types.
///
public enum HighContrastTheme
{
None,
WhiteOnBlack,
BlackOnWhite
}
///
/// Color palette for high contrast mode.
///
public class HighContrastColors
{
public SKColor Background { get; set; }
public SKColor Foreground { get; set; }
public SKColor Accent { get; set; }
public SKColor Border { get; set; }
public SKColor Error { get; set; }
public SKColor Success { get; set; }
public SKColor Warning { get; set; }
public SKColor Link { get; set; }
public SKColor LinkVisited { get; set; }
public SKColor Selection { get; set; }
public SKColor SelectionText { get; set; }
public SKColor DisabledText { get; set; }
public SKColor DisabledBackground { get; set; }
}
///
/// Event args for high contrast mode changes.
///
public class HighContrastChangedEventArgs : EventArgs
{
///
/// Gets whether high contrast mode is enabled.
///
public bool IsEnabled { get; }
///
/// Gets the current theme.
///
public HighContrastTheme Theme { get; }
public HighContrastChangedEventArgs(bool isEnabled, HighContrastTheme theme)
{
IsEnabled = isEnabled;
Theme = theme;
}
}