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//! On iOS, an app built with the iOS 27 SDK must declare a scene in its Info.plist, with
10//! [`IOS_SCENE_DELEGATE`] as the scene's delegate: iOS 27 stops at launch an app that does
11//! not, and winit 0.30 does not put its window in a scene by itself (#37).
12//!
13//! ```yaml
14//! UIApplicationSceneManifest:
15//!   UIApplicationSupportsMultipleScenes: false
16//!   UISceneConfigurations:
17//!     UIWindowSceneSessionRoleApplication:
18//!       - UISceneConfigurationName: Default
19//!         UISceneDelegateClassName: CorundumSceneDelegate
20//! ```
21//!
22//! ```no_run
23//! use corundum_framework_gui::{eframe, egui, window};
24//!
25//! struct Example;
26//!
27//! impl eframe::App for Example {
28//!     fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
29//!         ui.label("Hello");
30//!     }
31//! }
32//!
33//! fn main() -> eframe::Result<()> {
34//!     window::run(
35//!         window::WindowOptions::new("Example").inner_size([800.0, 500.0]),
36//!         |_cc| Box::new(Example),
37//!     )
38//! }
39//! ```
40
41use crate::gpu;
42
43#[cfg(all(target_os = "android", feature = "android"))]
44mod android;
45#[cfg(target_os = "ios")]
46mod ios;
47// Used by the iOS window; kept platform-free so its tests run on a desktop.
48#[cfg_attr(not(target_os = "ios"), allow(dead_code))]
49mod marked_text;
50// Used by the Android window; kept platform-free so its tests run on a desktop.
51#[cfg_attr(not(all(target_os = "android", feature = "android")), allow(dead_code))]
52mod text_input;
53
54#[cfg(all(target_os = "android", feature = "android"))]
55pub use android::{AndroidApp, run_android};
56
57/// The class of the scene delegate an iOS app names in its Info.plist's scene manifest
58/// (`UISceneDelegateClassName`). It shows the window once the app's scene connects.
59pub const IOS_SCENE_DELEGATE: &str = "CorundumSceneDelegate";
60
61/// Sets the egui context's fonts, once, before the app is built.
62pub type InstallFonts = Box<dyn FnOnce(&egui::Context)>;
63
64/// A desktop window: its title, its size and its fonts.
65pub struct WindowOptions {
66    title: String,
67    inner_size: [f32; 2],
68    min_inner_size: [f32; 2],
69    fonts: Option<InstallFonts>,
70    #[cfg(feature = "serde")]
71    store: Option<crate::store::Store>,
72}
73
74impl WindowOptions {
75    /// A window titled `title`, 900 × 600 and no smaller than 600 × 400.
76    pub fn new(title: impl Into<String>) -> WindowOptions {
77        WindowOptions {
78            title: title.into(),
79            inner_size: [900.0, 600.0],
80            min_inner_size: [600.0, 400.0],
81            fonts: None,
82            #[cfg(feature = "serde")]
83            store: None,
84        }
85    }
86
87    /// The window's size when it opens.
88    pub fn inner_size(mut self, size: [f32; 2]) -> WindowOptions {
89        self.inner_size = size;
90        self
91    }
92
93    /// The smallest the window can be made.
94    pub fn min_inner_size(mut self, size: [f32; 2]) -> WindowOptions {
95        self.min_inner_size = size;
96        self
97    }
98
99    /// Set the fonts with `install` instead of the default: egui's own, with a CJK font
100    /// the OS has behind them ([`fonts::install_system_cjk_fallback`](crate::fonts::install_system_cjk_fallback)).
101    pub fn fonts(mut self, install: impl FnOnce(&egui::Context) + 'static) -> WindowOptions {
102        self.fonts = Some(Box::new(install));
103        self
104    }
105
106    /// Keep corundum's file in `store`: the look saved there is installed before the app's first
107    /// frame, and the [`Shell`](crate::Shell) takes the rail's order saved there; from then
108    /// on [`look::apply`](crate::look::apply) and the shell's menu save their changes there.
109    #[cfg(feature = "serde")]
110    pub fn remember(mut self, store: crate::store::Store) -> WindowOptions {
111        self.store = Some(store);
112        self
113    }
114
115    /// `create`, after attaching corundum's store to the context when there is one.
116    #[cfg_attr(not(feature = "serde"), allow(unused_mut))]
117    pub(crate) fn wrap_create<'f>(
118        &mut self,
119        create: impl FnOnce(&eframe::CreationContext<'_>) -> Box<dyn eframe::App> + 'f,
120    ) -> impl FnOnce(&eframe::CreationContext<'_>) -> Box<dyn eframe::App> + 'f {
121        #[cfg(feature = "serde")]
122        let store = self.store.take();
123        move |cc| {
124            #[cfg(feature = "serde")]
125            if let Some(store) = store {
126                crate::store::attach(&cc.egui_ctx, store);
127            }
128            create(cc)
129        }
130    }
131}
132
133/// Open a desktop window and run it until it closes.
134///
135/// Installs [`gpu::install_panic_hook`], opens the window on wgpu with
136/// [`gpu::wgpu_options`], and builds the app with `create` once the window exists.
137pub fn run(
138    options: WindowOptions,
139    create: impl FnOnce(&eframe::CreationContext<'_>) -> Box<dyn eframe::App>,
140) -> eframe::Result<()> {
141    let mut options = options;
142    let create = options.wrap_create(create);
143    let WindowOptions {
144        title,
145        inner_size,
146        min_inner_size,
147        fonts,
148        ..
149    } = options;
150    gpu::install_panic_hook();
151    let native = eframe::NativeOptions {
152        viewport: egui::ViewportBuilder::default()
153            .with_inner_size(inner_size)
154            .with_min_inner_size(min_inner_size)
155            .with_title(&title),
156        wgpu_options: gpu::wgpu_options(),
157        ..Default::default()
158    };
159    open(&title, native, fonts, create)
160}
161
162/// Run eframe with `native` as the caller built it, building the app with `create` once the
163/// window exists: reports the renderer ([`gpu::on_render_state`]) and installs `fonts`, or
164/// the default ones when `None`. [`run`] is this for a desktop window.
165pub fn open(
166    title: &str,
167    native: eframe::NativeOptions,
168    fonts: Option<InstallFonts>,
169    create: impl FnOnce(&eframe::CreationContext<'_>) -> Box<dyn eframe::App>,
170) -> eframe::Result<()> {
171    // Before the event loop starts: UIKit makes the app's scene as it launches (#37).
172    #[cfg(target_os = "ios")]
173    ios::scene::adopt();
174    eframe::run_native(
175        title,
176        native,
177        Box::new(move |cc| {
178            gpu::on_render_state(cc.wgpu_render_state.as_ref());
179            match fonts {
180                Some(install) => install(&cc.egui_ctx),
181                None => {
182                    crate::fonts::install_system_cjk_fallback(&cc.egui_ctx);
183                }
184            }
185            let app = create(cc);
186            // eframe stretches its surface over an iPhone's screen (#20): see `ios`.
187            #[cfg(target_os = "ios")]
188            let app = ios::full_screen(cc, app);
189            Ok(app)
190        }),
191    )
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    #[test]
199    fn options_default_and_override_the_window_size() {
200        let options = WindowOptions::new("Example");
201        assert_eq!(options.title, "Example");
202        assert_eq!(options.inner_size, [900.0, 600.0]);
203        assert_eq!(options.min_inner_size, [600.0, 400.0]);
204        assert!(options.fonts.is_none());
205        let options = options
206            .inner_size([1000.0, 640.0])
207            .min_inner_size([720.0, 440.0])
208            .fonts(|_| {});
209        assert_eq!(options.inner_size, [1000.0, 640.0]);
210        assert_eq!(options.min_inner_size, [720.0, 440.0]);
211        assert!(options.fonts.is_some());
212    }
213}