Skip to main content

miniquad_ply/
lib.rs

1#![doc = include_str!("../README.md")]
2#![allow(
3    clippy::collapsible_if,
4    clippy::collapsible_else_if,
5    clippy::unused_unit,
6    clippy::identity_op,
7    clippy::missing_safety_doc
8)]
9
10pub mod conf;
11mod event;
12pub mod fs;
13pub mod graphics;
14pub mod native;
15use std::collections::HashMap;
16use std::ops::{Index, IndexMut};
17
18#[cfg(feature = "log-impl")]
19pub mod log;
20
21pub use event::*;
22
23pub use graphics::*;
24
25mod default_icon;
26
27pub use native::gl;
28
29#[derive(Clone)]
30pub(crate) struct ResourceManager<T> {
31    id: usize,
32    resources: HashMap<usize, T>,
33}
34
35impl<T> Default for ResourceManager<T> {
36    fn default() -> Self {
37        Self {
38            id: 0,
39            resources: HashMap::new(),
40        }
41    }
42}
43
44impl<T> ResourceManager<T> {
45    pub fn add(&mut self, resource: T) -> usize {
46        self.resources.insert(self.id, resource);
47        self.id += 1;
48        self.id - 1
49    }
50
51    pub fn remove(&mut self, id: usize) -> T {
52        // Let it crash if the resource is not found
53        self.resources.remove(&id).unwrap()
54    }
55}
56
57impl<T> Index<usize> for ResourceManager<T> {
58    type Output = T;
59    fn index(&self, index: usize) -> &Self::Output {
60        &self.resources[&index]
61    }
62}
63
64impl<T> IndexMut<usize> for ResourceManager<T> {
65    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
66        self.resources.get_mut(&index).unwrap()
67    }
68}
69
70pub mod date {
71    #[cfg(not(target_arch = "wasm32"))]
72    pub fn now() -> f64 {
73        use std::time::SystemTime;
74
75        let time = SystemTime::now()
76            .duration_since(SystemTime::UNIX_EPOCH)
77            .unwrap_or_else(|e| panic!("{}", e));
78        time.as_secs_f64()
79    }
80
81    #[cfg(target_arch = "wasm32")]
82    pub fn now() -> f64 {
83        use crate::native;
84
85        unsafe { native::wasm::now() }
86    }
87}
88
89pub type Context = dyn RenderingBackend;
90
91use std::sync::{Mutex, OnceLock};
92
93static NATIVE_DISPLAY: OnceLock<Mutex<native::NativeDisplayData>> = OnceLock::new();
94
95fn set_display(display: native::NativeDisplayData) {
96    NATIVE_DISPLAY
97        .set(Mutex::new(display))
98        .unwrap_or_else(|_| panic!("NATIVE_DISPLAY already set"));
99}
100/// This for now is Android specific since the process can continue running but the display
101/// is restarted. We support reinitializing the display.
102fn set_or_replace_display(display: native::NativeDisplayData) {
103    if let Some(m) = NATIVE_DISPLAY.get() {
104        // Replace existing display
105        *m.lock().unwrap() = display;
106    } else {
107        // First time initialization
108        set_display(display);
109    }
110}
111fn native_display() -> &'static Mutex<native::NativeDisplayData> {
112    NATIVE_DISPLAY
113        .get()
114        .expect("Backend has not initialized NATIVE_DISPLAY yet.") //|| Mutex::new(Default::default()))
115}
116
117/// Window and associated to window rendering context related functions.
118/// in macroquad <= 0.3, it was ctx.screen_size(). Now it is window::screen_size()
119pub mod window {
120    use super::*;
121
122    /// The same as
123    /// ```ignore
124    /// if metal {
125    ///    Box::new(MetalContext::new())
126    /// } else {
127    ///   Box::new(GlContext::new())
128    /// };
129    /// ```
130    /// but under #[cfg] gate to avoid MetalContext on non-apple platforms
131    pub fn new_rendering_backend() -> Box<dyn RenderingBackend> {
132        #[cfg(target_vendor = "apple")]
133        {
134            if window::apple_gfx_api() == conf::AppleGfxApi::Metal {
135                Box::new(MetalContext::new())
136            } else {
137                Box::new(GlContext::new())
138            }
139        }
140        #[cfg(not(target_vendor = "apple"))]
141        Box::new(GlContext::new())
142    }
143
144    /// The current framebuffer size in pixels
145    /// NOTE: [High DPI Rendering](../conf/index.html#high-dpi-rendering)
146    pub fn screen_size() -> (f32, f32) {
147        let d = native_display().lock().unwrap();
148        (d.screen_width as f32, d.screen_height as f32)
149    }
150
151    /// The dpi scaling factor (window pixels to framebuffer pixels)
152    /// NOTE: [High DPI Rendering](../conf/index.html#high-dpi-rendering)
153    pub fn dpi_scale() -> f32 {
154        let d = native_display().lock().unwrap();
155        d.dpi_scale
156    }
157
158    /// True when high_dpi was requested and actually running in a high-dpi scenario
159    /// NOTE: [High DPI Rendering](../conf/index.html#high-dpi-rendering)
160    pub fn high_dpi() -> bool {
161        let d = native_display().lock().unwrap();
162        d.high_dpi
163    }
164
165    pub fn blocking_event_loop() -> bool {
166        let d = native_display().lock().unwrap();
167        d.blocking_event_loop
168    }
169
170    /// This function simply quits the application without
171    /// giving the user a chance to intervene. Usually this might
172    /// be called when the user clicks the 'Ok' button in a 'Really Quit?'
173    /// dialog box
174    /// Window might not be actually closed right away (exit(0) might not
175    /// happen in the order_quit implmentation) and execution might continue for some time after
176    /// But the window is going to be inevitably closed at some point.
177    pub fn order_quit() {
178        let mut d = native_display().lock().unwrap();
179        d.quit_ordered = true;
180    }
181
182    /// Shortcut for `order_quit`. Will add a legacy attribute at some point.
183    pub fn quit() {
184        order_quit()
185    }
186
187    /// Calling request_quit() will trigger "quit_requested_event" event , giving
188    /// the user code a chance to intervene and cancel the pending quit process
189    /// (for instance to show a 'Really Quit?' dialog box).
190    /// If the event handler callback does nothing, the application will be quit as usual.
191    /// To prevent this, call the function "cancel_quit()"" from inside the event handler.
192    pub fn request_quit() {
193        let mut d = native_display().lock().unwrap();
194        d.quit_requested = true;
195    }
196
197    /// Cancels a pending quit request, either initiated
198    /// by the user clicking the window close button, or programmatically
199    /// by calling "request_quit()". The only place where calling this
200    /// function makes sense is from inside the event handler callback when
201    /// the "quit_requested_event" event has been received
202    pub fn cancel_quit() {
203        let mut d = native_display().lock().unwrap();
204        d.quit_requested = false;
205    }
206    /// Capture mouse cursor to the current window
207    /// On WASM this will automatically hide cursor
208    /// On desktop this will bound cursor to windows border
209    /// NOTICE: on desktop cursor will not be automatically released after window lost focus
210    ///         so set_cursor_grab(false) on window's focus lost is recommended.
211    /// TODO: implement window focus events
212    pub fn set_cursor_grab(grab: bool) {
213        let d = native_display().lock().unwrap();
214        #[cfg(target_os = "android")]
215        {
216            (d.native_requests)(native::Request::SetCursorGrab(grab));
217        }
218
219        #[cfg(not(target_os = "android"))]
220        {
221            d.native_requests
222                .send(native::Request::SetCursorGrab(grab))
223                .unwrap();
224        }
225    }
226
227    /// With `conf.platform.blocking_event_loop`, `schedule_update` called from an
228    /// event handler makes draw()/update() functions to be called without waiting
229    /// for a next event.
230    ///
231    /// Does nothing without `conf.platform.blocking_event_loop`.
232    pub fn schedule_update() {
233        #[cfg(all(target_os = "android", not(target_arch = "wasm32")))]
234        {
235            let d = native_display().lock().unwrap();
236            (d.native_requests)(native::Request::ScheduleUpdate);
237        }
238
239        #[cfg(not(any(target_arch = "wasm32", target_os = "android")))]
240        {
241            let d = native_display().lock().unwrap();
242            d.native_requests
243                .send(native::Request::ScheduleUpdate)
244                .unwrap();
245        }
246
247        #[cfg(target_arch = "wasm32")]
248        unsafe {
249            native::wasm::sapp_schedule_update();
250        }
251    }
252
253    /// Show or hide the mouse cursor
254    pub fn show_mouse(shown: bool) {
255        let d = native_display().lock().unwrap();
256        #[cfg(target_os = "android")]
257        {
258            (d.native_requests)(native::Request::ShowMouse(shown));
259        }
260
261        #[cfg(not(target_os = "android"))]
262        {
263            d.native_requests
264                .send(native::Request::ShowMouse(shown))
265                .unwrap();
266        }
267    }
268
269    /// Set the mouse cursor icon.
270    pub fn set_mouse_cursor(cursor_icon: CursorIcon) {
271        let d = native_display().lock().unwrap();
272        #[cfg(target_os = "android")]
273        {
274            (d.native_requests)(native::Request::SetMouseCursor(cursor_icon));
275        }
276
277        #[cfg(not(target_os = "android"))]
278        {
279            d.native_requests
280                .send(native::Request::SetMouseCursor(cursor_icon))
281                .unwrap();
282        }
283    }
284
285    /// Set the application's window size.
286    pub fn set_window_size(new_width: u32, new_height: u32) {
287        let d = native_display().lock().unwrap();
288        #[cfg(target_os = "android")]
289        {
290            (d.native_requests)(native::Request::SetWindowSize {
291                new_width,
292                new_height,
293            });
294        }
295
296        #[cfg(not(target_os = "android"))]
297        {
298            d.native_requests
299                .send(native::Request::SetWindowSize {
300                    new_width,
301                    new_height,
302                })
303                .unwrap();
304        }
305    }
306
307    pub fn set_window_position(new_x: u32, new_y: u32) {
308        let d = native_display().lock().unwrap();
309        #[cfg(target_os = "android")]
310        {
311            (d.native_requests)(native::Request::SetWindowPosition { new_x, new_y });
312        }
313
314        #[cfg(not(target_os = "android"))]
315        {
316            d.native_requests
317                .send(native::Request::SetWindowPosition { new_x, new_y })
318                .unwrap();
319        }
320    }
321
322    /// Get the position of the window.
323    /// TODO: implement for other platforms
324    #[cfg(any(target_os = "windows", target_os = "linux"))]
325    pub fn get_window_position() -> (u32, u32) {
326        let d = native_display().lock().unwrap();
327        d.screen_position
328    }
329
330    pub fn set_fullscreen(fullscreen: bool) {
331        let d = native_display().lock().unwrap();
332        #[cfg(target_os = "android")]
333        {
334            (d.native_requests)(native::Request::SetFullscreen(fullscreen));
335        }
336
337        #[cfg(not(target_os = "android"))]
338        {
339            d.native_requests
340                .send(native::Request::SetFullscreen(fullscreen))
341                .unwrap();
342        }
343    }
344
345    /// Get current OS clipboard value
346    pub fn clipboard_get() -> Option<String> {
347        let mut d = native_display().lock().unwrap();
348        d.clipboard.get()
349    }
350
351    /// Save value to OS clipboard
352    pub fn clipboard_set(data: &str) {
353        let mut d = native_display().lock().unwrap();
354        d.clipboard.set(data)
355    }
356    pub fn dropped_file_count() -> usize {
357        let d = native_display().lock().unwrap();
358        d.dropped_files.bytes.len()
359    }
360    pub fn dropped_file_bytes(index: usize) -> Option<Vec<u8>> {
361        let d = native_display().lock().unwrap();
362        d.dropped_files.bytes.get(index).cloned()
363    }
364    pub fn dropped_file_path(index: usize) -> Option<std::path::PathBuf> {
365        let d = native_display().lock().unwrap();
366        d.dropped_files.paths.get(index).cloned()
367    }
368
369    /// Show/hide onscreen keyboard.
370    /// Only works on Android right now.
371    pub fn show_keyboard(show: bool) {
372        let d = native_display().lock().unwrap();
373        #[cfg(target_os = "android")]
374        {
375            (d.native_requests)(native::Request::ShowKeyboard(show));
376        }
377
378        #[cfg(not(target_os = "android"))]
379        {
380            d.native_requests
381                .send(native::Request::ShowKeyboard(show))
382                .unwrap();
383        }
384    }
385
386    /// Set the position of the IME candidate window.
387    /// The position is in window client coordinates (pixels).
388    /// This should be called when the text cursor moves to keep the IME
389    /// candidate window near the insertion point.
390    pub fn set_ime_position(x: i32, y: i32) {
391        let d = native_display().lock().unwrap();
392        #[cfg(target_os = "android")]
393        {
394            let _ = (x, y); // IME position not applicable on Android
395        }
396
397        #[cfg(not(target_os = "android"))]
398        {
399            d.native_requests
400                .send(native::Request::SetImePosition { x, y })
401                .unwrap();
402        }
403    }
404
405    /// Enable or disable IME (Input Method Editor) for the window.
406    /// When enabled, the IME will process keyboard input for CJK text input.
407    /// When disabled, keyboard events are sent directly to the application,
408    /// which is useful for game controls (e.g., WASD movement).
409    ///
410    /// # Arguments
411    /// * `enabled` - `true` to enable IME (for text input), `false` to disable (for game controls)
412    pub fn set_ime_enabled(enabled: bool) {
413        let d = native_display().lock().unwrap();
414        #[cfg(target_os = "android")]
415        {
416            let _ = enabled; // IME control not applicable on Android
417        }
418
419        #[cfg(not(target_os = "android"))]
420        {
421            d.native_requests
422                .send(native::Request::SetImeEnabled(enabled))
423                .unwrap();
424        }
425    }
426
427    #[cfg(target_vendor = "apple")]
428    pub fn apple_gfx_api() -> crate::conf::AppleGfxApi {
429        let d = native_display().lock().unwrap();
430        d.gfx_api
431    }
432    #[cfg(target_vendor = "apple")]
433    pub fn apple_view() -> crate::native::apple::frameworks::ObjcId {
434        let d = native_display().lock().unwrap();
435        d.view
436    }
437    #[cfg(target_os = "ios")]
438    pub fn apple_view_ctrl() -> crate::native::apple::frameworks::ObjcId {
439        let d = native_display().lock().unwrap();
440        d.view_ctrl
441    }
442
443    /// Get the main window HWND as a raw pointer (`*mut c_void`).
444    ///
445    /// Returns `null_mut()` if the window has not been created yet.
446    /// Only available on Windows.
447    #[cfg(target_os = "windows")]
448    pub fn windows_hwnd() -> *mut std::ffi::c_void {
449        crate::native_display().lock().unwrap().hwnd
450    }
451}
452
453#[derive(Debug, Copy, Clone, PartialEq, Hash, Eq)]
454pub enum CursorIcon {
455    Default,
456    Help,
457    Pointer,
458    Wait,
459    Crosshair,
460    Text,
461    Move,
462    NotAllowed,
463    EWResize,
464    NSResize,
465    NESWResize,
466    NWSEResize,
467}
468
469/// Start miniquad.
470pub fn start<F>(conf: conf::Conf, f: F)
471where
472    F: 'static + FnOnce() -> Box<dyn EventHandler>,
473{
474    #[cfg(target_os = "linux")]
475    {
476        let mut f = Some(f);
477        let f = &mut f;
478        match conf.platform.linux_backend {
479            conf::LinuxBackend::X11Only => {
480                native::linux_x11::run(&conf, f).expect("X11 backend failed")
481            }
482            conf::LinuxBackend::WaylandOnly => {
483                native::linux_wayland::run(&conf, f).expect("Wayland backend failed")
484            }
485            conf::LinuxBackend::X11WithWaylandFallback => {
486                if let Err(err) = native::linux_x11::run(&conf, f) {
487                    eprintln!("{err:?}");
488                    eprintln!("Failed to initialize through X11! Trying wayland instead");
489                    native::linux_wayland::run(&conf, f);
490                }
491            }
492            conf::LinuxBackend::WaylandWithX11Fallback => {
493                if native::linux_wayland::run(&conf, f).is_none() {
494                    eprintln!("Failed to initialize through wayland! Trying X11 instead");
495                    native::linux_x11::run(&conf, f).unwrap()
496                }
497            }
498        }
499    }
500
501    #[cfg(target_os = "android")]
502    unsafe {
503        native::android::run(conf, f);
504    }
505
506    #[cfg(target_arch = "wasm32")]
507    {
508        native::wasm::run(&conf, f);
509    }
510
511    #[cfg(target_os = "windows")]
512    {
513        native::windows::run(&conf, f);
514    }
515
516    #[cfg(target_os = "macos")]
517    unsafe {
518        native::macos::run(conf, f);
519    }
520
521    #[cfg(target_os = "ios")]
522    unsafe {
523        native::ios::run(conf, f);
524    }
525}