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