freya_hooks/
use_init_native_platform.rs

1use dioxus_core::{
2    prelude::{
3        consume_context,
4        provide_context,
5        spawn,
6    },
7    use_hook,
8};
9use dioxus_hooks::use_context_provider;
10use dioxus_signals::{
11    Readable,
12    Signal,
13    Writable,
14};
15use freya_core::types::NativePlatformReceiver;
16
17use crate::use_init_asset_cacher;
18
19#[derive(Clone)]
20pub struct NavigationMark(bool);
21
22impl NavigationMark {
23    pub fn allowed(&self) -> bool {
24        self.0
25    }
26
27    pub fn set_allowed(&mut self, allowed: bool) {
28        self.0 = allowed;
29    }
30}
31
32#[derive(Clone, Copy)]
33pub struct UsePlatformEvents {
34    pub navigation_mark: Signal<NavigationMark>,
35}
36
37/// Keep some native features (focused element, preferred theme, etc) on sync between the platform and the components
38pub fn use_init_native_platform() -> UsePlatformEvents {
39    // Inithe global asset cacher
40    use_init_asset_cacher();
41
42    // Init the NavigationMark signal
43    let navigation_mark = use_context_provider(|| Signal::new(NavigationMark(true)));
44
45    // Init the signals with platform values
46    use_hook(|| {
47        let mut platform_receiver = consume_context::<NativePlatformReceiver>();
48        let platform_state = platform_receiver.borrow();
49
50        let mut preferred_theme = Signal::new(platform_state.preferred_theme);
51        let mut focused_id = Signal::new(platform_state.focused_accessibility_id);
52        let mut focused_node = Signal::new(platform_state.focused_accessibility_node.clone());
53        let mut navigation_mode = Signal::new(platform_state.navigation_mode);
54        let mut information = Signal::new(platform_state.information);
55
56        drop(platform_state);
57
58        // Listen for any changes during the execution of the app
59        spawn(async move {
60            while platform_receiver.changed().await.is_ok() {
61                let state = platform_receiver.borrow();
62                if *focused_id.peek() != state.focused_accessibility_id {
63                    *focused_id.write() = state.focused_accessibility_id;
64                }
65
66                if *focused_node.peek() != state.focused_accessibility_node {
67                    *focused_node.write() = state.focused_accessibility_node.clone();
68                }
69
70                if *preferred_theme.peek() != state.preferred_theme {
71                    *preferred_theme.write() = state.preferred_theme;
72                }
73
74                if *navigation_mode.peek() != state.navigation_mode {
75                    *navigation_mode.write() = state.navigation_mode;
76                }
77
78                if *information.peek() != state.information {
79                    *information.write() = state.information;
80                }
81            }
82        });
83
84        provide_context(preferred_theme);
85        provide_context(navigation_mode);
86        provide_context(information);
87        provide_context(focused_id);
88        provide_context(focused_node);
89    });
90
91    UsePlatformEvents { navigation_mark }
92}
93
94#[cfg(test)]
95mod test {
96    use freya::prelude::*;
97    use freya_core::accessibility::ACCESSIBILITY_ROOT_ID;
98    use freya_testing::prelude::*;
99
100    #[tokio::test]
101    pub async fn focus_accessibility() {
102        #[allow(non_snake_case)]
103        fn OtherChild() -> Element {
104            let mut focus_manager = use_focus();
105
106            rsx!(rect {
107                a11y_id: focus_manager.attribute(),
108                width: "100%",
109                height: "50%",
110                onclick: move |_| focus_manager.request_focus(),
111            })
112        }
113
114        fn use_focus_app() -> Element {
115            rsx!(
116                rect {
117                    width: "100%",
118                    height: "100%",
119                    OtherChild {}
120                    OtherChild {}
121                }
122            )
123        }
124
125        let mut utils = launch_test_with_config(
126            use_focus_app,
127            TestingConfig::<()> {
128                size: (100.0, 100.0).into(),
129                ..TestingConfig::default()
130            },
131        );
132
133        // Initial state
134        utils.wait_for_update().await;
135        assert_eq!(utils.focus_id(), ACCESSIBILITY_ROOT_ID);
136
137        // Click on the first rect
138        utils.click_cursor((5., 5.)).await;
139
140        // First rect is now focused
141        utils.wait_for_update().await;
142        utils.wait_for_update().await;
143        let first_focus_id = utils.focus_id();
144        assert_ne!(first_focus_id, ACCESSIBILITY_ROOT_ID);
145
146        // Click on the second rect
147        utils.click_cursor((5., 75.)).await;
148
149        // Second rect is now focused
150        utils.wait_for_update().await;
151        utils.wait_for_update().await;
152        let second_focus_id = utils.focus_id();
153        assert_ne!(first_focus_id, second_focus_id);
154        assert_ne!(second_focus_id, ACCESSIBILITY_ROOT_ID);
155    }
156
157    #[tokio::test]
158    pub async fn uncontrolled_focus_accessibility() {
159        #[allow(non_snake_case)]
160        fn OtherChild() -> Element {
161            let focus = use_focus();
162            rsx!(rect {
163                a11y_id: focus.attribute(),
164                width: "100%",
165                height: "50%",
166            })
167        }
168
169        fn use_focus_app() -> Element {
170            rsx!(
171                rect {
172                    width: "100%",
173                    height: "100%",
174                    OtherChild {},
175                    OtherChild {}
176                }
177            )
178        }
179
180        let mut utils = launch_test_with_config(
181            use_focus_app,
182            TestingConfig::<()> {
183                size: (100.0, 100.0).into(),
184                ..TestingConfig::default()
185            },
186        );
187
188        // Initial state
189        utils.wait_for_update().await;
190        assert_eq!(utils.focus_id(), ACCESSIBILITY_ROOT_ID);
191
192        // Navigate to the first rect
193        utils.push_event(TestEvent::Keyboard {
194            name: EventName::KeyDown,
195            key: Key::Tab,
196            code: Code::Tab,
197            modifiers: Modifiers::default(),
198        });
199        utils.wait_for_update().await;
200
201        // First rect is now focused
202        utils.wait_for_update().await;
203        utils.wait_for_update().await;
204        let first_focus_id = utils.focus_id();
205        assert_ne!(first_focus_id, ACCESSIBILITY_ROOT_ID);
206
207        // Navigate to the second rect
208        utils.push_event(TestEvent::Keyboard {
209            name: EventName::KeyDown,
210            key: Key::Tab,
211            code: Code::Tab,
212            modifiers: Modifiers::default(),
213        });
214        utils.wait_for_update().await;
215
216        utils.wait_for_update().await;
217        utils.wait_for_update().await;
218        let second_focus_id = utils.focus_id();
219        assert_ne!(first_focus_id, second_focus_id);
220        assert_ne!(second_focus_id, ACCESSIBILITY_ROOT_ID);
221    }
222
223    #[tokio::test]
224    pub async fn auto_focus_accessibility() {
225        fn use_focus_app() -> Element {
226            let focus_1 = use_focus();
227            let focus_2 = use_focus();
228            rsx!(
229                rect {
230                    a11y_id: focus_1.attribute(),
231                    a11y_auto_focus: "true",
232                }
233                rect {
234                    a11y_id: focus_2.attribute(),
235                    a11y_auto_focus: "true",
236                }
237            )
238        }
239
240        let mut utils = launch_test_with_config(
241            use_focus_app,
242            TestingConfig::<()> {
243                size: (100.0, 100.0).into(),
244                ..TestingConfig::default()
245            },
246        );
247
248        utils.wait_for_update().await;
249        assert_ne!(utils.focus_id(), ACCESSIBILITY_ROOT_ID); // Will focus the second rect
250    }
251}