Skip to main content

paiagram/
main.rs

1//! The entrypoint of the application
2
3use std::path::PathBuf;
4
5use bevy::ecs::system::RunSystemOnce;
6use bevy::log::LogPlugin;
7use bevy::prelude::*;
8use clap::Parser;
9use paiagram_core::settings::UserPreferences;
10use paiagram_core::trip::class;
11use paiagram_core::{entry, graph, i18n, import, problems, route, settings, station, trip};
12use paiagram_rw::read::ReadPlugin;
13use paiagram_rw::save::SavePlugin;
14use serde::Deserialize;
15
16struct PaiagramApp {
17    bevy_app: App,
18}
19
20impl PaiagramApp {
21    fn new(cc: &eframe::CreationContext) -> Self {
22        // Modifications to the defualt style
23        // TODO: define own styles
24        cc.egui_ctx.global_style_mut(|style| {
25            style.spacing.window_margin = egui::Margin::same(2);
26            style.interaction.selectable_labels = false;
27        });
28        paiagram_ui::apply_custom_fonts(&cc.egui_ctx);
29        if let Some(render_state) = cc.wgpu_render_state.as_ref() {
30            cc.egui_ctx.data_mut(|data| {
31                data.insert_temp(
32                    egui::Id::new("wgpu_adapter_info"),
33                    eframe::egui_wgpu::adapter_info_summary(&render_state.adapter.get_info()),
34                );
35                data.insert_temp(
36                    egui::Id::new("wgpu_target_format"),
37                    render_state.target_format,
38                );
39                let msaa_samples = if cfg!(target_arch = "wasm32") {
40                    1_u32
41                } else {
42                    4_u32
43                };
44                data.insert_temp(egui::Id::new("wgpu_msaa_samples"), msaa_samples);
45            });
46        }
47        let mut app = App::new();
48        app.add_plugins(MinimalPlugins);
49        app.add_plugins(LogPlugin::default());
50        app.add_plugins((
51            paiagram_ui::UiPlugin,
52            entry::EntryPlugin,
53            graph::GraphPlugin,
54            route::RoutePlugin,
55            import::ImportPlugin,
56            trip::TripPlugin,
57            station::StationPlugin,
58            settings::SettingsPlugin,
59            problems::ProblemsPlugin,
60            class::ClassPlugin,
61            ReadPlugin,
62            SavePlugin,
63            bevy::asset::AssetPlugin::default(), // TODO: remove
64            bevy::scene::ScenePlugin,
65        ));
66        info!("Initialized Bevy App.");
67        #[cfg(not(target_arch = "wasm32"))]
68        let args = Arguments::parse();
69        #[cfg(target_arch = "wasm32")]
70        let args = parse_web_arguments();
71        if let Err(e) = app.world_mut().run_system_once_with(handle_args, args) {
72            error!("Failed to web arguments: {:?}", e);
73        } else {
74            info!("Arguments handled successfully.");
75        }
76        Self { bevy_app: app }
77    }
78}
79
80#[cfg(target_arch = "wasm32")]
81fn parse_web_arguments() -> Arguments {
82    if let Some(search) =
83        eframe::web_sys::window().and_then(|window| window.location().search().ok())
84    {
85        info!(?search, "Handling web args...");
86        let query = search.strip_prefix('?').unwrap_or(&search);
87        match serde_html_form::from_str::<Arguments>(&query) {
88            Ok(args) => return args,
89            Err(error) => error!("Failed to parse web args: {error}"),
90        }
91    }
92
93    Arguments::default()
94}
95
96impl eframe::App for PaiagramApp {
97    fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] {
98        egui::Rgba::TRANSPARENT.to_array()
99    }
100    fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
101        self.bevy_app.update();
102        // TODO: remove this
103        self.bevy_app
104            .world_mut()
105            .resource_mut::<UserPreferences>()
106            .dark_mode = match ui.system_theme() {
107            None => false,
108            Some(egui::Theme::Dark) => true,
109            Some(egui::Theme::Light) => false,
110        };
111        paiagram_ui::show_ui(ui, self.bevy_app.world_mut(), frame.info().cpu_usage);
112    }
113}
114
115/// Arguments for the application.
116#[derive(Parser, Default, Deserialize)]
117#[command(version, about, long_about = None)]
118struct Arguments {
119    #[arg(
120        short = 'o',
121        long = "open",
122        help = "Path to a .paiagram file (or any other compatible file formats) to open on startup",
123        num_args = 1..
124    )]
125    #[serde(default)]
126    open: Option<Vec<PathBuf>>,
127    #[arg(long = "locale", help = "Set the localization")]
128    #[serde(default)]
129    locale: Option<String>,
130}
131
132fn handle_args(
133    args: In<Arguments>,
134    mut commands: Commands,
135    mut languages: ResMut<UserPreferences>,
136) {
137    for path in args.open.iter().flatten() {
138        #[cfg(target_arch = "wasm32")]
139        {
140            commands.trigger(import::DownloadFile {
141                url: path.to_string_lossy().into_owned(),
142            });
143            continue;
144        }
145
146        #[cfg(not(target_arch = "wasm32"))]
147        {
148            let content = match std::fs::read(path) {
149                Ok(c) => c,
150                Err(e) => {
151                    error!("Could not open {:?}: {:?}", path, e);
152                    continue;
153                }
154            };
155            if let Err(e) = import::load_and_trigger(path, content, &mut commands) {
156                error!("Could not load {:?}: {:#?}", path, e);
157                continue;
158            }
159        }
160    }
161    if let Some(locale) = args.locale.as_deref() {
162        if languages.lang.set(locale) {
163            info!("Successfully set language {}", locale);
164        } else {
165            error!("Unknown language identifier: {}", locale);
166        }
167    }
168}
169
170#[cfg(not(target_arch = "wasm32"))]
171fn main() -> eframe::Result<()> {
172    i18n::init();
173    let native_options = eframe::NativeOptions {
174        viewport: egui::ViewportBuilder::default()
175            .with_title("Drawer")
176            .with_app_id("Paiagram")
177            .with_inner_size([1280.0, 720.0]),
178        renderer: eframe::Renderer::Wgpu,
179        wgpu_options: eframe::egui_wgpu::WgpuConfiguration {
180            desired_maximum_frame_latency: Some(2),
181            ..default()
182        },
183        multisampling: 4,
184        ..default()
185    };
186    eframe::run_native(
187        "Paiagram Drawer",
188        native_options,
189        Box::new(|cc| Ok(Box::new(PaiagramApp::new(cc)))),
190    )
191}
192
193#[cfg(target_arch = "wasm32")]
194use wasm_bindgen::prelude::*;
195
196#[cfg(target_arch = "wasm32")]
197#[derive(Clone)]
198#[wasm_bindgen]
199pub struct WebHandle {
200    runner: eframe::WebRunner,
201}
202
203#[cfg(target_arch = "wasm32")]
204fn main() {
205    i18n::init();
206    use eframe::wasm_bindgen::JsCast as _;
207    use eframe::web_sys;
208
209    let web_options = eframe::WebOptions::default();
210
211    wasm_bindgen_futures::spawn_local(async {
212        let document = web_sys::window()
213            .expect("No window")
214            .document()
215            .expect("No document");
216
217        let canvas = if let Some(canvas) = document.get_element_by_id("paiagram_canvas") {
218            canvas
219                .dyn_into::<web_sys::HtmlCanvasElement>()
220                .expect("paiagram_canvas was not a HtmlCanvasElement")
221        } else {
222            let canvas = document
223                .create_element("canvas")
224                .expect("Failed to create canvas element");
225            canvas.set_id("paiagram_canvas");
226
227            canvas
228                .set_attribute("style", "display: block; width: 100%; height: 100%;")
229                .ok();
230
231            let body = document.body().expect("Failed to get document body");
232            body.set_attribute(
233                "style",
234                "margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden;",
235            )
236            .ok();
237
238            let html = document.document_element().expect("No document element");
239            html.set_attribute(
240                "style",
241                "margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden;",
242            )
243            .ok();
244
245            body.append_child(&canvas).expect("Failed to append canvas");
246            canvas
247                .dyn_into::<web_sys::HtmlCanvasElement>()
248                .expect("Failed to cast canvas")
249        };
250
251        let start_result = eframe::WebRunner::new()
252            .start(
253                canvas,
254                web_options,
255                Box::new(|cc| Ok(Box::new(PaiagramApp::new(cc)))),
256            )
257            .await;
258
259        if let Some(loading_text) = document.get_element_by_id("loading_text") {
260            match start_result {
261                Ok(_) => {
262                    loading_text.remove();
263                }
264                Err(e) => {
265                    loading_text.set_inner_html(
266                        "<p> The app has crashed. See the developer console for details. </p>",
267                    );
268                    panic!("Failed to start eframe: {e:?}");
269                }
270            }
271        }
272    });
273}