Skip to main content

freya_core/
platform.rs

1use std::{
2    borrow::Cow,
3    cell::RefCell,
4    rc::Rc,
5};
6
7use bytes::Bytes;
8pub use mundy::{
9    AccentColor,
10    Srgba,
11};
12use rustc_hash::FxHashMap;
13use torin::prelude::Size2D;
14
15use crate::{
16    accessibility::id::AccessibilityId,
17    current_context::CurrentContext,
18    prelude::{
19        GlobalContexts,
20        State,
21        try_consume_root_context,
22    },
23    user_event::{
24        GlobalUserEvent,
25        UserEvent,
26    },
27};
28
29/// How the user is navigating the application.
30#[derive(Clone, Copy, PartialEq, Eq, Default, Debug, Hash)]
31pub enum NavigationMode {
32    /// Navigation is driven by a pointing device or touch input.
33    #[default]
34    NotKeyboard,
35    /// Navigation is driven by keyboard input.
36    Keyboard,
37}
38
39/// The color theme preferred by the operating system.
40#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
41pub enum PreferredTheme {
42    /// The operating system prefers a light color theme.
43    #[default]
44    Light,
45    /// The operating system prefers a dark color theme.
46    Dark,
47}
48
49/// Operating system targeted by an application.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum TargetPlatform {
52    /// Microsoft Windows.
53    Windows,
54    /// Apple macOS.
55    MacOs,
56    /// Linux.
57    Linux,
58    /// Google Android.
59    Android,
60    /// Apple iOS.
61    Ios,
62    /// An operating system Freya does not recognize.
63    Unknown,
64}
65
66impl TargetPlatform {
67    /// Returns the target platform from the current Freya context, or detects the compile target.
68    pub fn get() -> Self {
69        CurrentContext::try_with(|_| try_consume_root_context())
70            .flatten()
71            .unwrap_or_else(Self::detect)
72    }
73
74    /// Detects the operating system of the compile target.
75    pub fn detect() -> Self {
76        if cfg!(target_os = "windows") {
77            Self::Windows
78        } else if cfg!(target_os = "macos") {
79            Self::MacOs
80        } else if cfg!(target_os = "linux") {
81            Self::Linux
82        } else if cfg!(target_os = "android") {
83            Self::Android
84        } else if cfg!(target_os = "ios") {
85            Self::Ios
86        } else {
87            Self::Unknown
88        }
89    }
90
91    /// Returns whether this is a desktop operating system.
92    pub fn is_desktop(&self) -> bool {
93        matches!(self, Self::Windows | Self::MacOs | Self::Linux)
94    }
95
96    /// Returns whether this is a mobile operating system.
97    pub fn is_mobile(&self) -> bool {
98        matches!(self, Self::Android | Self::Ios)
99    }
100}
101
102/// Application-wide platform api.
103#[derive(Clone)]
104pub struct Platform {
105    sender: Rc<dyn Fn(GlobalUserEvent)>,
106    windows: Rc<RefCell<FxHashMap<u64, PlatformWindow>>>,
107}
108
109impl Platform {
110    pub fn new(sender: impl Fn(GlobalUserEvent) + 'static) -> Self {
111        Self {
112            sender: Rc::new(sender),
113            windows: Rc::default(),
114        }
115    }
116
117    #[track_caller]
118    pub fn get() -> Self {
119        GlobalContexts::get().get_context()
120    }
121
122    /// Returns the live window with this ID, panicking if it has closed or does not exist.
123    #[track_caller]
124    pub fn window(&self, window_id: impl Into<u64>) -> PlatformWindow {
125        let window_id = window_id.into();
126        self.try_window(window_id)
127            .unwrap_or_else(|| panic!("Window {window_id} does not exist or has closed."))
128    }
129
130    /// Returns the live window with this ID, or `None` if it has closed or does not exist.
131    pub fn try_window(&self, window_id: impl Into<u64>) -> Option<PlatformWindow> {
132        self.windows.borrow().get(&window_id.into()).cloned()
133    }
134
135    /// Returns the window from whose component, event handler, or scoped task this is called.
136    #[track_caller]
137    pub fn current_window(&self) -> PlatformWindow {
138        let window_id: CurrentWindowId = CurrentContext::try_with(|_| try_consume_root_context())
139            .flatten()
140            .expect(concat!(
141                "Platform::current_window() requires a Freya window context. ",
142                "In `spawn_global`, use `Platform::get().window(window_id)` or capture ",
143                "the current window before spawning."
144            ));
145        self.window(window_id.0)
146    }
147
148    pub fn register_window(&self, window: PlatformWindow) {
149        self.windows.borrow_mut().insert(window.id, window);
150    }
151
152    pub fn unregister_window(&self, window_id: impl Into<u64>) {
153        let _removed_window = self.windows.borrow_mut().remove(&window_id.into());
154    }
155
156    pub fn send(&self, event: GlobalUserEvent) {
157        (self.sender)(event)
158    }
159
160    /// Open a URL with the system's default browser.
161    pub fn open_url(&self, url: impl Into<String>) {
162        self.send(GlobalUserEvent::OpenUrl(url.into()));
163    }
164
165    /// Request application exit.
166    pub fn exit(&self) {
167        self.send(GlobalUserEvent::Exit);
168    }
169
170    /// Load a font at runtime in every window.
171    ///
172    /// # Example
173    ///
174    /// ```rust,no_run
175    /// use freya_core::prelude::*;
176    ///
177    /// fn load_rubik() {
178    ///     let font = std::fs::read("./Rubik.ttf").expect("Failed to read the font file.");
179    ///     Platform::get().load_font("Rubik", font);
180    /// }
181    /// ```
182    pub fn load_font(&self, font_name: impl Into<Cow<'static, str>>, font_data: impl Into<Bytes>) {
183        self.send(GlobalUserEvent::LoadFont {
184            font_name: font_name.into(),
185            font_data: font_data.into(),
186        });
187    }
188}
189
190/// The ID of the window whose component context is currently executing.
191#[derive(Clone, Copy)]
192pub struct CurrentWindowId(pub u64);
193
194/// State and APIs for a given window.
195///
196/// # Example
197///
198/// ```rust,no_run
199/// use freya_core::prelude::*;
200///
201/// fn app() -> impl IntoElement {
202///     let window = Platform::get().current_window();
203///     let root_size = *window.root_size.read();
204///     let preferred_theme = *window.preferred_theme.read();
205///
206///     format!(
207///         "Window: {:.0} × {:.0}, preferred theme: {preferred_theme:?}",
208///         root_size.width, root_size.height,
209///     )
210/// }
211/// ```
212#[derive(Clone)]
213pub struct PlatformWindow {
214    pub id: u64,
215    /// The [`AccessibilityId`] of the currently focused node in this window.
216    pub focused_accessibility_id: State<AccessibilityId>,
217    /// Accessibility data for this window's currently focused node.
218    pub focused_accessibility_node: State<accesskit::Node>,
219    /// This window's size in logical pixels.
220    pub root_size: State<Size2D>,
221    /// This window's effective rendering scale factor.
222    pub scale_factor: State<f64>,
223    /// The custom scale factor, changed through [`Self::set_custom_scale_factor`].
224    pub custom_scale_factor: State<f64>,
225    /// The current [`NavigationMode`] in this window.
226    pub navigation_mode: State<NavigationMode>,
227    /// The color theme preferred by the operating system for this window.
228    pub preferred_theme: State<PreferredTheme>,
229    /// Whether this window has operating-system focus.
230    pub is_app_focused: State<bool>,
231    /// The operating system's accent color, when available.
232    pub accent_color: State<AccentColor>,
233    /// Dispatches [`UserEvent`]s to this window's renderer.
234    pub sender: Rc<dyn Fn(UserEvent)>,
235}
236
237impl PlatformWindow {
238    pub fn send(&self, event: UserEvent) {
239        (self.sender)(event)
240    }
241
242    /// Request a render of this window.
243    pub fn request_redraw(&self) {
244        self.send(UserEvent::RequestRedraw);
245    }
246
247    /// Request a custom rendering scale factor for this window.
248    pub fn set_custom_scale_factor(&self, custom_scale_factor: f64) {
249        self.send(UserEvent::SetCustomScaleFactor(custom_scale_factor));
250    }
251}