revue 2.71.1

A Vue-style TUI framework for Rust with CSS styling
Documentation
//! Signal-based global accessibility state
//!
//! Provides a reactive accessibility system that can be used throughout the application.
//! Uses signals for reactive state management of accessibility preferences and announcements.
//!
//! # Example
//!
//! ```rust,ignore
//! use revue::prelude::*;
//!
//! // Announce a message
//! announce("Button clicked");
//!
//! // Announce immediately (interrupts)
//! announce_now("Error: Invalid input");
//!
//! // Check preferences
//! if prefers_reduced_motion() {
//!     // Skip animation
//! }
//!
//! if is_high_contrast() {
//!     // Use high contrast colors
//! }
//! ```

use std::sync::{Arc, OnceLock, RwLock};

pub use super::accessibility::Priority;
use super::accessibility::{AccessibilityManager, Announcement};

/// Global accessibility state
fn get_accessibility() -> &'static Arc<RwLock<AccessibilityManager>> {
    static ACCESSIBILITY: OnceLock<Arc<RwLock<AccessibilityManager>>> = OnceLock::new();
    ACCESSIBILITY.get_or_init(|| Arc::new(RwLock::new(AccessibilityManager::new())))
}

/// Queue a polite announcement (waits for idle)
///
/// Use this for status updates and non-urgent information.
/// Screen readers will read this after finishing current speech.
///
/// # Example
///
/// ```rust,ignore
/// announce("5 items loaded");
/// announce("Selection changed to item 3");
/// ```
pub fn announce(message: impl Into<String>) {
    let mut manager = get_accessibility().write().unwrap_or_else(|e| {
        crate::log_error!("Accessibility lock poisoned: {e}");
        e.into_inner()
    });
    manager.announce_polite(message);
}

/// Queue an assertive announcement (interrupts immediately)
///
/// Use this for urgent information like errors or alerts.
/// Screen readers will interrupt current speech to read this.
///
/// # Example
///
/// ```rust,ignore
/// announce_now("Error: Form validation failed");
/// announce_now("Alert: Connection lost");
/// ```
pub fn announce_now(message: impl Into<String>) {
    let mut manager = get_accessibility().write().unwrap_or_else(|e| {
        crate::log_error!("Accessibility lock poisoned: {e}");
        e.into_inner()
    });
    manager.announce_assertive(message);
}

/// Get pending announcements and clear the queue
///
/// Call this during the render/tick loop to process announcements.
/// Returns a vector of pending announcements.
pub fn take_announcements() -> Vec<Announcement> {
    let mut manager = get_accessibility().write().unwrap_or_else(|e| {
        crate::log_error!("Accessibility lock poisoned: {e}");
        e.into_inner()
    });
    let announcements = manager.pending_announcements().to_vec();
    manager.clear_announcements();
    announcements
}

/// Check if there are pending announcements
pub fn has_announcements() -> bool {
    let manager = get_accessibility().read().unwrap_or_else(|e| {
        crate::log_warn!("Accessibility read lock poisoned: {e}");
        e.into_inner()
    });
    !manager.pending_announcements().is_empty()
}

/// Set reduced motion preference
///
/// When enabled, animations should be skipped or minimized.
pub fn set_reduced_motion(enabled: bool) {
    let mut manager = get_accessibility().write().unwrap_or_else(|e| {
        crate::log_error!("Accessibility lock poisoned: {e}");
        e.into_inner()
    });
    manager.set_reduce_motion(enabled);
}

/// Check if reduced motion is preferred
///
/// Returns true if the user prefers reduced motion.
/// Widgets should check this before running animations.
///
/// # Example
///
/// ```rust,ignore
/// if prefers_reduced_motion() {
///     // Use instant transition
///     progress.set_instant(true);
/// } else {
///     // Use animated transition
///     progress.set_animated(true);
/// }
/// ```
pub fn prefers_reduced_motion() -> bool {
    let manager = get_accessibility().read().unwrap_or_else(|e| {
        crate::log_warn!("Accessibility read lock poisoned: {e}");
        e.into_inner()
    });
    manager.prefers_reduced_motion()
}

/// Set high contrast mode
///
/// When enabled, widgets should use higher contrast colors.
pub fn set_high_contrast(enabled: bool) {
    let mut manager = get_accessibility().write().unwrap_or_else(|e| {
        crate::log_error!("Accessibility lock poisoned: {e}");
        e.into_inner()
    });
    manager.set_high_contrast(enabled);
}

/// Check if high contrast mode is enabled
///
/// Returns true if high contrast mode is active.
/// Widgets should use the high-contrast theme variants when this is true.
///
/// # Example
///
/// ```rust,ignore
/// if is_high_contrast() {
///     // Use high-contrast-dark or high-contrast-light theme
///     set_theme_by_id("high-contrast-dark");
/// }
/// ```
pub fn is_high_contrast() -> bool {
    let manager = get_accessibility().read().unwrap_or_else(|e| {
        crate::log_warn!("Accessibility read lock poisoned: {e}");
        e.into_inner()
    });
    manager.is_high_contrast()
}

/// Enable or disable the accessibility system
pub fn set_accessibility_enabled(enabled: bool) {
    let mut manager = get_accessibility().write().unwrap_or_else(|e| {
        crate::log_error!("Accessibility lock poisoned: {e}");
        e.into_inner()
    });
    manager.set_enabled(enabled);
}

/// Check if accessibility is enabled
pub fn is_accessibility_enabled() -> bool {
    let manager = get_accessibility().read().unwrap_or_else(|e| {
        crate::log_warn!("Accessibility read lock poisoned: {e}");
        e.into_inner()
    });
    manager.is_enabled()
}

// =============================================================================
// Widget-specific announcement helpers
// =============================================================================

/// Announce a button activation
pub fn announce_button_clicked(label: &str) {
    announce(format!("{} activated", label));
}

/// Announce a checkbox state change
pub fn announce_checkbox_changed(label: &str, checked: bool) {
    let state = if checked { "checked" } else { "unchecked" };
    announce(format!("{}, {}", label, state));
}

/// Announce a selection change in a list
pub fn announce_list_selection(label: &str, index: usize, total: usize) {
    announce(format!("{}, {} of {}", label, index + 1, total));
}

/// Announce a tab change
pub fn announce_tab_changed(label: &str, index: usize, total: usize) {
    announce(format!("Tab: {}, {} of {}", label, index + 1, total));
}

/// Announce an error
pub fn announce_error(message: &str) {
    announce_now(format!("Error: {}", message));
}

/// Announce a successful action
pub fn announce_success(message: &str) {
    announce(format!("Success: {}", message));
}

/// Announce loading state
pub fn announce_loading(context: &str) {
    announce(format!("Loading {}", context));
}

/// Announce loading complete
pub fn announce_loaded(context: &str, count: Option<usize>) {
    match count {
        Some(n) => announce(format!("{} loaded, {} items", context, n)),
        None => announce(format!("{} loaded", context)),
    }
}

/// Announce a dialog opening
pub fn announce_dialog_opened(title: &str) {
    announce_now(format!("Dialog: {}", title));
}

/// Announce a dialog closing
pub fn announce_dialog_closed() {
    announce("Dialog closed");
}

/// Announce form validation error
pub fn announce_validation_error(field: &str, message: &str) {
    announce_now(format!("{}: {}", field, message));
}

/// Announce focus moved to a region
pub fn announce_focus_region(region: &str) {
    announce(format!("Moved to {}", region));
}

/// Announce progress update
pub fn announce_progress(percent: u8, context: &str) {
    announce(format!("{}: {}%", context, percent));
}

/// Announce progress complete
pub fn announce_progress_complete(context: &str) {
    announce(format!("{}: Complete", context));
}

#[cfg(test)]
mod tests {
    use super::*;
    use serial_test::serial;

    // Helper to reset global state before each test
    fn setup() {
        set_accessibility_enabled(true);
        let _ = take_announcements(); // Clear any pending
        set_reduced_motion(false);
        set_high_contrast(false);
    }

    #[test]
    #[serial]
    fn test_announce() {
        setup();
        announce("Test message");
        let announcements = take_announcements();
        assert_eq!(announcements.len(), 1);
        assert_eq!(announcements[0].message, "Test message");
        assert_eq!(announcements[0].priority, Priority::Polite);
    }

    #[test]
    #[serial]
    fn test_announce_now() {
        setup();
        announce_now("Urgent message");
        let announcements = take_announcements();
        assert_eq!(announcements.len(), 1);
        assert_eq!(announcements[0].message, "Urgent message");
        assert_eq!(announcements[0].priority, Priority::Assertive);
    }

    #[test]
    #[serial]
    fn test_take_clears_queue() {
        setup();
        announce("Message 1");
        announce("Message 2");

        let first = take_announcements();
        assert_eq!(first.len(), 2);

        let second = take_announcements();
        assert!(second.is_empty());
    }

    #[test]
    #[serial]
    fn test_reduced_motion() {
        setup();
        set_reduced_motion(true);
        assert!(prefers_reduced_motion());

        set_reduced_motion(false);
        assert!(!prefers_reduced_motion());
    }

    #[test]
    #[serial]
    fn test_high_contrast() {
        setup();
        set_high_contrast(true);
        assert!(is_high_contrast());

        set_high_contrast(false);
        assert!(!is_high_contrast());
    }

    #[test]
    #[serial]
    fn test_disabled_no_announcements() {
        setup();
        set_accessibility_enabled(false);
        announce("Should not appear");
        let announcements = take_announcements();
        assert!(announcements.is_empty());

        // Re-enable for other tests
        set_accessibility_enabled(true);
    }

    #[test]
    #[serial]
    fn test_button_clicked() {
        setup();
        announce_button_clicked("Submit");
        let announcements = take_announcements();
        assert_eq!(announcements[0].message, "Submit activated");
    }

    #[test]
    #[serial]
    fn test_checkbox_changed() {
        setup();
        announce_checkbox_changed("Accept terms", true);
        let announcements = take_announcements();
        assert_eq!(announcements[0].message, "Accept terms, checked");
    }

    #[test]
    #[serial]
    fn test_list_selection() {
        setup();
        announce_list_selection("Item A", 0, 5);
        let announcements = take_announcements();
        assert_eq!(announcements[0].message, "Item A, 1 of 5");
    }

    #[test]
    #[serial]
    fn test_error_announcement() {
        setup();
        announce_error("Invalid email");
        let announcements = take_announcements();
        assert_eq!(announcements[0].message, "Error: Invalid email");
        assert_eq!(announcements[0].priority, Priority::Assertive);
    }

    #[test]
    #[serial]
    fn test_progress_announcement() {
        setup();
        announce_progress(50, "Upload");
        let announcements = take_announcements();
        assert_eq!(announcements[0].message, "Upload: 50%");
    }
}