Skip to main content

corundum_framework_gui/window/
mod.rs

1//! A desktop window: eframe on wgpu, with the renderer set up by [`gpu`](crate::gpu) and a
2//! CJK font behind egui's own.
3//!
4//! [`run`] is the whole of opening an app's desktop window; [`open`] is its lower half, for
5//! an entry that builds its own [`eframe::NativeOptions`]. With the `android` feature,
6//! `run_android` opens the window on an Android phone. The sapphire apps
7//! and asteria open their windows through here, so a renderer fix reaches all of them.
8//!
9//! ```no_run
10//! use corundum_framework_gui::{eframe, egui, window};
11//!
12//! struct Example;
13//!
14//! impl eframe::App for Example {
15//!     fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
16//!         ui.label("Hello");
17//!     }
18//! }
19//!
20//! fn main() -> eframe::Result<()> {
21//!     window::run(
22//!         window::WindowOptions::new("Example").inner_size([800.0, 500.0]),
23//!         |_cc| Box::new(Example),
24//!     )
25//! }
26//! ```
27
28use crate::gpu;
29
30#[cfg(all(target_os = "android", feature = "android"))]
31mod android;
32#[cfg(target_os = "ios")]
33mod ios;
34// Used by the Android window; kept platform-free so its tests run on a desktop.
35#[cfg_attr(not(all(target_os = "android", feature = "android")), allow(dead_code))]
36mod text_input;
37
38#[cfg(all(target_os = "android", feature = "android"))]
39pub use android::{AndroidApp, run_android};
40
41/// Sets the egui context's fonts, once, before the app is built.
42pub type InstallFonts = Box<dyn FnOnce(&egui::Context)>;
43
44/// A desktop window: its title, its size and its fonts.
45pub struct WindowOptions {
46    title: String,
47    inner_size: [f32; 2],
48    min_inner_size: [f32; 2],
49    fonts: Option<InstallFonts>,
50}
51
52impl WindowOptions {
53    /// A window titled `title`, 900 × 600 and no smaller than 600 × 400.
54    pub fn new(title: impl Into<String>) -> WindowOptions {
55        WindowOptions {
56            title: title.into(),
57            inner_size: [900.0, 600.0],
58            min_inner_size: [600.0, 400.0],
59            fonts: None,
60        }
61    }
62
63    /// The window's size when it opens.
64    pub fn inner_size(mut self, size: [f32; 2]) -> WindowOptions {
65        self.inner_size = size;
66        self
67    }
68
69    /// The smallest the window can be made.
70    pub fn min_inner_size(mut self, size: [f32; 2]) -> WindowOptions {
71        self.min_inner_size = size;
72        self
73    }
74
75    /// Set the fonts with `install` instead of the default: egui's own, with a CJK font
76    /// the OS has behind them ([`fonts::install_system_cjk_fallback`](crate::fonts::install_system_cjk_fallback)).
77    pub fn fonts(mut self, install: impl FnOnce(&egui::Context) + 'static) -> WindowOptions {
78        self.fonts = Some(Box::new(install));
79        self
80    }
81}
82
83/// Open a desktop window and run it until it closes.
84///
85/// Installs [`gpu::install_panic_hook`], opens the window on wgpu with
86/// [`gpu::wgpu_options`], and builds the app with `create` once the window exists.
87pub fn run(
88    options: WindowOptions,
89    create: impl FnOnce(&eframe::CreationContext<'_>) -> Box<dyn eframe::App>,
90) -> eframe::Result<()> {
91    let WindowOptions {
92        title,
93        inner_size,
94        min_inner_size,
95        fonts,
96    } = options;
97    gpu::install_panic_hook();
98    let native = eframe::NativeOptions {
99        viewport: egui::ViewportBuilder::default()
100            .with_inner_size(inner_size)
101            .with_min_inner_size(min_inner_size)
102            .with_title(&title),
103        wgpu_options: gpu::wgpu_options(),
104        ..Default::default()
105    };
106    open(&title, native, fonts, create)
107}
108
109/// Run eframe with `native` as the caller built it, building the app with `create` once the
110/// window exists: reports the renderer ([`gpu::on_render_state`]) and installs `fonts`, or
111/// the default ones when `None`. [`run`] is this for a desktop window.
112pub fn open(
113    title: &str,
114    native: eframe::NativeOptions,
115    fonts: Option<InstallFonts>,
116    create: impl FnOnce(&eframe::CreationContext<'_>) -> Box<dyn eframe::App>,
117) -> eframe::Result<()> {
118    eframe::run_native(
119        title,
120        native,
121        Box::new(move |cc| {
122            gpu::on_render_state(cc.wgpu_render_state.as_ref());
123            match fonts {
124                Some(install) => install(&cc.egui_ctx),
125                None => {
126                    crate::fonts::install_system_cjk_fallback(&cc.egui_ctx);
127                }
128            }
129            let app = create(cc);
130            // eframe stretches its surface over an iPhone's screen (#20): see `ios`.
131            #[cfg(target_os = "ios")]
132            let app = ios::full_screen(cc, app);
133            Ok(app)
134        }),
135    )
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn options_default_and_override_the_window_size() {
144        let options = WindowOptions::new("Example");
145        assert_eq!(options.title, "Example");
146        assert_eq!(options.inner_size, [900.0, 600.0]);
147        assert_eq!(options.min_inner_size, [600.0, 400.0]);
148        assert!(options.fonts.is_none());
149        let options = options
150            .inner_size([1000.0, 640.0])
151            .min_inner_size([720.0, 440.0])
152            .fonts(|_| {});
153        assert_eq!(options.inner_size, [1000.0, 640.0]);
154        assert_eq!(options.min_inner_size, [720.0, 440.0]);
155        assert!(options.fonts.is_some());
156    }
157}