1use 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 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(), 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 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#[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}