use teksilo_core::ScreenReaderState;
#[derive(Debug, Clone, PartialEq)]
pub struct AccessibilityPreferences {
pub high_contrast: bool,
pub reduced_motion: bool,
pub text_scale_factor: f64,
pub screen_reader: ScreenReaderState,
}
impl Default for AccessibilityPreferences {
fn default() -> Self {
Self {
high_contrast: false,
reduced_motion: false,
text_scale_factor: 1.0,
screen_reader: ScreenReaderState::Unknown,
}
}
}
impl AccessibilityPreferences {
pub fn query() -> Self {
platform::query()
}
pub fn prefers_large_text(&self) -> bool {
self.text_scale_factor > 1.0
}
}
#[cfg(target_os = "linux")]
mod platform {
use super::{AccessibilityPreferences, ScreenReaderState};
use crate::linux_helpers::{Desktop, detect_desktop, read_gsettings};
use crate::linux_portal::{read_bool_property, read_portal_u32};
use crate::os_theme::kde;
use teksilo_kde_scheme::KdeConfigLocations;
pub(super) fn query() -> AccessibilityPreferences {
let mut prefs = AccessibilityPreferences::default();
if let Some(v) = read_portal_u32("org.freedesktop.appearance", "contrast") {
prefs.high_contrast = v == 1;
}
prefs.reduced_motion = reduced_motion(
read_portal_u32("org.freedesktop.appearance", "reduced-motion"),
kde_reduced_motion,
|| read_gsettings("org.gnome.desktop.interface", "enable-animations"),
);
if !prefs.high_contrast
&& let Some(theme) = read_gsettings("org.gnome.desktop.interface", "gtk-theme")
{
prefs.high_contrast = theme.contains("HighContrast");
}
if !prefs.high_contrast
&& let Some(val) = read_gsettings("org.gnome.desktop.a11y.interface", "high-contrast")
{
prefs.high_contrast = val == "true";
}
if let Some(val) = read_gsettings("org.gnome.desktop.interface", "text-scaling-factor")
&& let Ok(scale) = val.parse::<f64>()
{
prefs.text_scale_factor = scale;
}
prefs.screen_reader = query_screen_reader();
prefs
}
pub(super) fn reduced_motion(
portal: Option<u32>,
kde: impl FnOnce() -> Option<bool>,
enable_animations: impl FnOnce() -> Option<String>,
) -> bool {
portal == Some(1) || kde() == Some(true) || enable_animations().as_deref() == Some("false")
}
fn kde_reduced_motion() -> Option<bool> {
if !matches!(detect_desktop(), Desktop::Kde) {
return None;
}
let locations = KdeConfigLocations::from_env(kde::process_env);
kde::kde_prefers_reduced_motion(&locations, &kde::process_env, kde::read_file)
}
fn query_screen_reader() -> ScreenReaderState {
match read_bool_property(
"org.a11y.Bus",
"/org/a11y/bus",
"org.a11y.Status",
"ScreenReaderEnabled",
) {
Some(true) => ScreenReaderState::Active,
Some(false) => ScreenReaderState::Inactive,
None => ScreenReaderState::Unknown,
}
}
}
#[cfg(target_os = "macos")]
mod platform {
use super::{AccessibilityPreferences, ScreenReaderState};
pub(super) fn query() -> AccessibilityPreferences {
let mut prefs = AccessibilityPreferences::default();
let workspace = objc2_app_kit::NSWorkspace::sharedWorkspace();
prefs.high_contrast = workspace.accessibilityDisplayShouldIncreaseContrast();
prefs.reduced_motion = workspace.accessibilityDisplayShouldReduceMotion();
prefs.screen_reader = if workspace.isVoiceOverEnabled() {
ScreenReaderState::Active
} else {
ScreenReaderState::Inactive
};
prefs
}
}
#[cfg(target_os = "windows")]
mod platform {
use super::{AccessibilityPreferences, ScreenReaderState};
pub(super) fn query() -> AccessibilityPreferences {
let high_contrast = query_high_contrast();
let (reduced_motion, text_scale) = query_ui_settings();
AccessibilityPreferences {
high_contrast,
reduced_motion,
text_scale_factor: text_scale,
screen_reader: query_screen_reader(),
}
}
fn query_screen_reader() -> ScreenReaderState {
use windows::Win32::UI::WindowsAndMessaging::{
SPI_GETSCREENREADER, SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS, SystemParametersInfoW,
};
use windows::core::BOOL;
unsafe {
let mut on = BOOL(0);
let ok = SystemParametersInfoW(
SPI_GETSCREENREADER,
0,
Some(&mut on as *mut BOOL as *mut _),
SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS(0),
);
match ok {
Ok(()) if on.as_bool() => ScreenReaderState::Active,
Ok(()) => ScreenReaderState::Inactive,
Err(_) => ScreenReaderState::Unknown,
}
}
}
fn query_high_contrast() -> bool {
use std::mem;
use windows::Win32::UI::Accessibility::HIGHCONTRASTW;
use windows::Win32::UI::WindowsAndMessaging::{
SPI_GETHIGHCONTRAST, SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS, SystemParametersInfoW,
};
unsafe {
let mut hc = HIGHCONTRASTW {
cbSize: mem::size_of::<HIGHCONTRASTW>() as u32,
..Default::default()
};
let ok = SystemParametersInfoW(
SPI_GETHIGHCONTRAST,
hc.cbSize,
Some(&mut hc as *mut _ as *mut _),
SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS(0),
);
if ok.is_ok() {
(hc.dwFlags.0 & 0x01) != 0
} else {
false
}
}
}
fn query_ui_settings() -> (bool, f64) {
use windows::UI::ViewManagement::UISettings;
let mut reduced_motion = false;
let mut text_scale = 1.0_f64;
if let Ok(settings) = UISettings::new() {
if let Ok(animations_enabled) = settings.AnimationsEnabled() {
reduced_motion = !animations_enabled;
}
if let Ok(scale) = settings.TextScaleFactor() {
text_scale = scale;
}
}
(reduced_motion, text_scale)
}
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
mod platform {
use super::AccessibilityPreferences;
pub(super) fn query() -> AccessibilityPreferences {
AccessibilityPreferences::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_preferences() {
let prefs = AccessibilityPreferences::default();
assert!(!prefs.high_contrast);
assert!(!prefs.reduced_motion);
assert!((prefs.text_scale_factor - 1.0).abs() < f64::EPSILON);
assert!(!prefs.prefers_large_text());
assert_eq!(prefs.screen_reader, ScreenReaderState::Unknown);
}
#[test]
fn large_text_threshold() {
let mut prefs = AccessibilityPreferences {
text_scale_factor: 1.25,
..AccessibilityPreferences::default()
};
assert!(prefs.prefers_large_text());
prefs.text_scale_factor = 1.0;
assert!(!prefs.prefers_large_text());
}
#[test]
fn query_does_not_panic() {
let prefs = AccessibilityPreferences::query();
assert!(prefs.text_scale_factor > 0.0);
assert!(matches!(
prefs.screen_reader,
ScreenReaderState::Unknown | ScreenReaderState::Inactive | ScreenReaderState::Active
));
}
#[cfg(target_os = "linux")]
#[test]
fn reduced_motion_is_asked_for_by_any_one_source() {
use platform::reduced_motion;
let no_kde = || None;
let yes = || Some(true);
let kde_no = || Some(false);
let no_gnome = || None;
let animations = |value: &'static str| move || Some(value.to_owned());
assert!(reduced_motion(Some(1), no_kde, no_gnome));
assert!(!reduced_motion(Some(0), no_kde, no_gnome));
assert!(!reduced_motion(None, no_kde, no_gnome));
assert!(reduced_motion(None, yes, no_gnome));
assert!(reduced_motion(Some(0), yes, animations("true")));
assert!(!reduced_motion(None, kde_no, no_gnome));
assert!(reduced_motion(None, kde_no, animations("false")));
assert!(reduced_motion(Some(0), no_kde, animations("false")));
assert!(!reduced_motion(None, kde_no, animations("true")));
assert!(reduced_motion(
Some(1),
|| panic!("KDE is not read once the portal asked"),
|| panic!("GNOME is not read once the portal asked"),
));
assert!(reduced_motion(None, yes, || panic!(
"GNOME is not read once KDE asked"
)));
}
#[test]
fn preferences_compare_on_the_screen_reader_too() {
let a = AccessibilityPreferences::default();
let b = AccessibilityPreferences {
screen_reader: ScreenReaderState::Active,
..AccessibilityPreferences::default()
};
assert_ne!(a, b);
}
}