freya-core 0.4.0-rc.7

Reactivity runtime, tree management, accessibility integration, rendering pipeline and more, for Freya
Documentation
use keyboard_types::{
    Key,
    Modifiers,
    NamedKey,
};

use crate::{
    accessibility::id::AccessibilityId,
    integration::{
        ACCESSIBILITY_ROOT_ID,
        AccessibilityGenerator,
    },
    platform::{
        NavigationMode,
        Platform,
    },
    prelude::{
        AccessibilityFocusStrategy,
        KeyboardEventData,
        Memo,
        ScreenReader,
        UserEvent,
        consume_root_context,
        use_hook,
        use_memo,
    },
};

#[derive(Clone, Copy)]
pub struct Focus {
    a11y_id: AccessibilityId,
}

impl Focus {
    pub fn create() -> Self {
        Self::new_for_id(Self::new_id())
    }

    pub fn new_for_id(a11y_id: AccessibilityId) -> Self {
        Self { a11y_id }
    }

    pub fn new_id() -> AccessibilityId {
        let accessibility_generator = consume_root_context::<AccessibilityGenerator>();
        AccessibilityId(accessibility_generator.new_id())
    }

    pub fn a11y_id(&self) -> AccessibilityId {
        self.a11y_id
    }

    pub fn is_focused(&self) -> bool {
        let platform = Platform::get();
        *platform.focused_accessibility_id.peek() == self.a11y_id
    }

    pub fn is_focused_with_keyboard(&self) -> bool {
        let platform = Platform::get();
        *platform.focused_accessibility_id.peek() == self.a11y_id
            && *platform.navigation_mode.peek() == NavigationMode::Keyboard
    }

    pub fn request_focus(&self) {
        Platform::get().send(UserEvent::FocusAccessibilityNode(
            AccessibilityFocusStrategy::Node(self.a11y_id),
        ));
    }

    pub fn request_unfocus(&self) {
        Platform::get().send(UserEvent::FocusAccessibilityNode(
            AccessibilityFocusStrategy::Node(ACCESSIBILITY_ROOT_ID),
        ));
    }

    pub fn is_pressed(event: &KeyboardEventData) -> bool {
        let is_space = matches!(event.key, Key::Character(ref s) if s == " ");
        let is_enter = event.key == Key::Named(NamedKey::Enter);

        if cfg!(target_os = "macos") {
            let screen_reader = ScreenReader::get();
            if screen_reader.is_on() {
                is_space
                    && event.modifiers.contains(Modifiers::CONTROL)
                    && event.modifiers.contains(Modifiers::ALT)
            } else {
                is_enter || is_space
            }
        } else {
            is_enter || is_space
        }
    }
}

pub fn use_focus() -> Focus {
    use_hook(Focus::create)
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum FocusStatus {
    Not,
    Pointer,
    Keyboard,
}

impl FocusStatus {
    pub fn is_focused(&self) -> bool {
        matches!(self, Self::Pointer | Self::Keyboard)
    }
}

pub fn use_focus_status(focus: Focus) -> Memo<FocusStatus> {
    use_memo(move || {
        let platform = Platform::get();
        let is_focused = *platform.focused_accessibility_id.read() == focus.a11y_id;
        let is_keyboard = *platform.navigation_mode.read() == NavigationMode::Keyboard;

        match (is_focused, is_keyboard) {
            (true, false) => FocusStatus::Pointer,
            (true, true) => FocusStatus::Keyboard,
            _ => FocusStatus::Not,
        }
    })
}