Skip to main content

freya_android/
lib.rs

1#![cfg(target_os = "android")]
2
3use freya_components::theming::{
4    hooks::use_init_theme,
5    themes::light_theme,
6};
7use freya_core::{
8    integration::*,
9    prelude::*,
10};
11use freya_winit::{
12    integration::is_ime_role,
13    plugins::{
14        FreyaPlugin,
15        PluginEvent,
16        PluginHandle,
17    },
18};
19use winit::platform::android::activity::AndroidApp;
20
21mod keyboard;
22mod status_bar;
23
24/// Extension trait for [`Platform`] providing Android-specific APIs.
25pub trait AndroidExt {
26    /// Set whether the Android status bar uses light appearance (dark icons for light backgrounds).
27    fn set_status_bar_light(&self, light: bool) -> Result<(), jni::errors::Error>;
28
29    /// Show the Android soft keyboard.
30    fn show_keyboard(&self) -> Result<(), jni::errors::Error>;
31
32    /// Hide the Android soft keyboard.
33    fn hide_keyboard(&self) -> Result<(), jni::errors::Error>;
34}
35
36impl AndroidExt for Platform {
37    fn set_status_bar_light(&self, light: bool) -> Result<(), jni::errors::Error> {
38        if let Some(app) = try_consume_root_context::<AndroidApp>() {
39            status_bar::set_status_bar_light(&app, light)
40        } else {
41            Ok(())
42        }
43    }
44
45    fn show_keyboard(&self) -> Result<(), jni::errors::Error> {
46        if let Some(app) = try_consume_root_context::<AndroidApp>() {
47            keyboard::show_keyboard(&app)
48        } else {
49            Ok(())
50        }
51    }
52
53    fn hide_keyboard(&self) -> Result<(), jni::errors::Error> {
54        if let Some(app) = try_consume_root_context::<AndroidApp>() {
55            keyboard::hide_keyboard(&app)
56        } else {
57            Ok(())
58        }
59    }
60}
61
62/// Freya plugin for Android integration.
63///
64/// Stores the [`AndroidApp`] handle, provides it as root context,
65/// and registers a root component that syncs the status bar
66/// appearance with the app theme and manages the soft keyboard.
67pub struct AndroidPlugin {
68    app: AndroidApp,
69}
70
71impl AndroidPlugin {
72    pub fn new(app: AndroidApp) -> Self {
73        Self { app }
74    }
75}
76
77impl FreyaPlugin for AndroidPlugin {
78    fn plugin_id(&self) -> &'static str {
79        "android"
80    }
81
82    fn on_event(&mut self, event: &mut PluginEvent, _handle: PluginHandle) {
83        if let PluginEvent::RunnerCreated { runner } = event {
84            let app = self.app.clone();
85            runner.provide_root_context(move || app);
86        }
87    }
88
89    fn root_component(&self, root: Element) -> Element {
90        AndroidRoot { inner: root }.into_element()
91    }
92}
93
94/// Root component that manages Android platform integration.
95#[derive(Clone)]
96struct AndroidRoot {
97    inner: Element,
98}
99
100impl PartialEq for AndroidRoot {
101    fn eq(&self, _other: &Self) -> bool {
102        true
103    }
104}
105
106impl Component for AndroidRoot {
107    fn render(&self) -> impl IntoElement {
108        let theme = use_init_theme(light_theme);
109
110        // Sync status bar appearance with theme
111        use_side_effect(move || {
112            let platform = Platform::get();
113            let is_light = theme.read().name == "light";
114            if let Err(err) = platform.set_status_bar_light(is_light) {
115                tracing::error!("Failed to set status bar appearance: {err:?}");
116            }
117        });
118
119        // Show/hide keyboard based on focused node type
120        use_side_effect(move || {
121            let platform = Platform::get();
122            let focused_node = platform.focused_accessibility_node.read();
123            let result = if is_ime_role(focused_node.role()) {
124                platform.show_keyboard()
125            } else {
126                platform.hide_keyboard()
127            };
128            if let Err(err) = result {
129                tracing::error!("Failed to toggle soft keyboard: {err:?}");
130            }
131        });
132
133        let on_global_pointer_down = move |_: Event<PointerEventData>| {
134            let platform = Platform::get();
135            if let Err(err) = platform.hide_keyboard() {
136                tracing::error!("Failed to hide soft keyboard: {err:?}");
137            }
138        };
139
140        rect()
141            .expanded()
142            .on_global_pointer_down(on_global_pointer_down)
143            .child(self.inner.clone())
144    }
145}