freya_core/accessibility/
focus.rs

1use keyboard_types::{
2    Key,
3    Modifiers,
4    NamedKey,
5};
6
7use crate::{
8    accessibility::id::AccessibilityId,
9    integration::{
10        ACCESSIBILITY_ROOT_ID,
11        AccessibilityGenerator,
12    },
13    platform::{
14        NavigationMode,
15        Platform,
16    },
17    prelude::{
18        AccessibilityFocusStrategy,
19        KeyboardEventData,
20        Memo,
21        ScreenReader,
22        UserEvent,
23        consume_root_context,
24        use_hook,
25        use_memo,
26    },
27};
28
29#[derive(Clone, Copy)]
30pub struct Focus {
31    a11y_id: AccessibilityId,
32}
33
34impl Focus {
35    pub fn create() -> Self {
36        Self::new_for_id(Self::new_id())
37    }
38
39    pub fn new_for_id(a11y_id: AccessibilityId) -> Self {
40        Self { a11y_id }
41    }
42
43    pub fn new_id() -> AccessibilityId {
44        let accessibility_generator = consume_root_context::<AccessibilityGenerator>();
45        AccessibilityId(accessibility_generator.new_id())
46    }
47
48    pub fn a11y_id(&self) -> AccessibilityId {
49        self.a11y_id
50    }
51
52    pub fn is_focused(&self) -> bool {
53        let platform = Platform::get();
54        *platform.focused_accessibility_id.peek() == self.a11y_id
55    }
56
57    pub fn is_focused_with_keyboard(&self) -> bool {
58        let platform = Platform::get();
59        *platform.focused_accessibility_id.peek() == self.a11y_id
60            && *platform.navigation_mode.peek() == NavigationMode::Keyboard
61    }
62
63    pub fn request_focus(&self) {
64        Platform::get().send(UserEvent::FocusAccessibilityNode(
65            AccessibilityFocusStrategy::Node(self.a11y_id),
66        ));
67    }
68
69    pub fn request_unfocus(&self) {
70        Platform::get().send(UserEvent::FocusAccessibilityNode(
71            AccessibilityFocusStrategy::Node(ACCESSIBILITY_ROOT_ID),
72        ));
73    }
74
75    pub fn is_pressed(event: &KeyboardEventData) -> bool {
76        let is_space = matches!(event.key, Key::Character(ref s) if s == " ");
77        let is_enter = event.key == Key::Named(NamedKey::Enter);
78
79        if cfg!(target_os = "macos") {
80            let screen_reader = ScreenReader::get();
81            if screen_reader.is_on() {
82                is_space
83                    && event.modifiers.contains(Modifiers::CONTROL)
84                    && event.modifiers.contains(Modifiers::ALT)
85            } else {
86                is_enter || is_space
87            }
88        } else {
89            is_enter || is_space
90        }
91    }
92}
93
94pub fn use_focus() -> Focus {
95    use_hook(Focus::create)
96}
97
98#[derive(Clone, Copy, Debug, PartialEq)]
99pub enum FocusStatus {
100    Not,
101    Pointer,
102    Keyboard,
103}
104
105pub fn use_focus_status(focus: Focus) -> Memo<FocusStatus> {
106    use_memo(move || {
107        let platform = Platform::get();
108        let is_focused = *platform.focused_accessibility_id.read() == focus.a11y_id;
109        let is_keyboard = *platform.navigation_mode.read() == NavigationMode::Keyboard;
110
111        match (is_focused, is_keyboard) {
112            (true, false) => FocusStatus::Pointer,
113            (true, true) => FocusStatus::Keyboard,
114            _ => FocusStatus::Not,
115        }
116    })
117}