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}