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    #[cfg(feature = "serde")]
51    store: Option<crate::store::Store>,
52}
53
54impl WindowOptions {
55    /// A window titled `title`, 900 × 600 and no smaller than 600 × 400.
56    pub fn new(title: impl Into<String>) -> WindowOptions {
57        WindowOptions {
58            title: title.into(),
59            inner_size: [900.0, 600.0],
60            min_inner_size: [600.0, 400.0],
61            fonts: None,
62            #[cfg(feature = "serde")]
63            store: None,
64        }
65    }
66
67    /// The window's size when it opens.
68    pub fn inner_size(mut self, size: [f32; 2]) -> WindowOptions {
69        self.inner_size = size;
70        self
71    }
72
73    /// The smallest the window can be made.
74    pub fn min_inner_size(mut self, size: [f32; 2]) -> WindowOptions {
75        self.min_inner_size = size;
76        self
77    }
78
79    /// Set the fonts with `install` instead of the default: egui's own, with a CJK font
80    /// the OS has behind them ([`fonts::install_system_cjk_fallback`](crate::fonts::install_system_cjk_fallback)).
81    pub fn fonts(mut self, install: impl FnOnce(&egui::Context) + 'static) -> WindowOptions {
82        self.fonts = Some(Box::new(install));
83        self
84    }
85
86    /// Keep corundum's file in `store`: the look saved there is installed before the app's first
87    /// frame, and the [`Shell`](crate::Shell) takes the rail's order saved there; from then
88    /// on [`look::apply`](crate::look::apply) and the shell's menu save their changes there.
89    #[cfg(feature = "serde")]
90    pub fn remember(mut self, store: crate::store::Store) -> WindowOptions {
91        self.store = Some(store);
92        self
93    }
94
95    /// `create`, after attaching corundum's store to the context when there is one.
96    #[cfg_attr(not(feature = "serde"), allow(unused_mut))]
97    pub(crate) fn wrap_create<'f>(
98        &mut self,
99        create: impl FnOnce(&eframe::CreationContext<'_>) -> Box<dyn eframe::App> + 'f,
100    ) -> impl FnOnce(&eframe::CreationContext<'_>) -> Box<dyn eframe::App> + 'f {
101        #[cfg(feature = "serde")]
102        let store = self.store.take();
103        move |cc| {
104            #[cfg(feature = "serde")]
105            if let Some(store) = store {
106                crate::store::attach(&cc.egui_ctx, store);
107            }
108            create(cc)
109        }
110    }
111}
112
113/// Open a desktop window and run it until it closes.
114///
115/// Installs [`gpu::install_panic_hook`], opens the window on wgpu with
116/// [`gpu::wgpu_options`], and builds the app with `create` once the window exists.
117pub fn run(
118    options: WindowOptions,
119    create: impl FnOnce(&eframe::CreationContext<'_>) -> Box<dyn eframe::App>,
120) -> eframe::Result<()> {
121    let mut options = options;
122    let create = options.wrap_create(create);
123    let WindowOptions {
124        title,
125        inner_size,
126        min_inner_size,
127        fonts,
128        ..
129    } = options;
130    gpu::install_panic_hook();
131    let native = eframe::NativeOptions {
132        viewport: egui::ViewportBuilder::default()
133            .with_inner_size(inner_size)
134            .with_min_inner_size(min_inner_size)
135            .with_title(&title),
136        wgpu_options: gpu::wgpu_options(),
137        ..Default::default()
138    };
139    open(&title, native, fonts, create)
140}
141
142/// Run eframe with `native` as the caller built it, building the app with `create` once the
143/// window exists: reports the renderer ([`gpu::on_render_state`]) and installs `fonts`, or
144/// the default ones when `None`. [`run`] is this for a desktop window.
145pub fn open(
146    title: &str,
147    native: eframe::NativeOptions,
148    fonts: Option<InstallFonts>,
149    create: impl FnOnce(&eframe::CreationContext<'_>) -> Box<dyn eframe::App>,
150) -> eframe::Result<()> {
151    eframe::run_native(
152        title,
153        native,
154        Box::new(move |cc| {
155            gpu::on_render_state(cc.wgpu_render_state.as_ref());
156            match fonts {
157                Some(install) => install(&cc.egui_ctx),
158                None => {
159                    crate::fonts::install_system_cjk_fallback(&cc.egui_ctx);
160                }
161            }
162            let app = create(cc);
163            // eframe stretches its surface over an iPhone's screen (#20): see `ios`.
164            #[cfg(target_os = "ios")]
165            let app = ios::full_screen(cc, app);
166            Ok(app)
167        }),
168    )
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn options_default_and_override_the_window_size() {
177        let options = WindowOptions::new("Example");
178        assert_eq!(options.title, "Example");
179        assert_eq!(options.inner_size, [900.0, 600.0]);
180        assert_eq!(options.min_inner_size, [600.0, 400.0]);
181        assert!(options.fonts.is_none());
182        let options = options
183            .inner_size([1000.0, 640.0])
184            .min_inner_size([720.0, 440.0])
185            .fonts(|_| {});
186        assert_eq!(options.inner_size, [1000.0, 640.0]);
187        assert_eq!(options.min_inner_size, [720.0, 440.0]);
188        assert!(options.fonts.is_some());
189    }
190}