use geo::point; use std::{ future::Future, io, ops::ControlFlow, sync::mpsc::{channel, Receiver, Sender}, }; use unic_langid::{langid, LanguageIdentifier}; use topola::{ interactor::activity::InteractiveInput, specctra::design::{LoadingError as SpecctraLoadingError, SpecctraDesign}, }; use crate::{ config::Config, error_dialog::ErrorDialog, menu_bar::MenuBar, status_bar::StatusBar, translator::Translator, viewport::Viewport, workspace::Workspace, }; pub struct App { config: Config, translator: Translator, content_channel: ( Sender>, Receiver>, ), viewport: Viewport, menu_bar: MenuBar, status_bar: StatusBar, error_dialog: ErrorDialog, maybe_workspace: Option, update_counter: f32, } impl Default for App { fn default() -> Self { Self { config: Config::default(), translator: Translator::new(langid!("en-US")), content_channel: channel(), viewport: Viewport::new(), menu_bar: MenuBar::new(), status_bar: StatusBar::new(), error_dialog: ErrorDialog::new(), maybe_workspace: None, update_counter: 0.0, } } } impl App { /// Called once on start. pub fn new(cc: &eframe::CreationContext<'_>, langid: LanguageIdentifier) -> Self { let mut this = Self { translator: Translator::new(langid), ..Default::default() }; // Load previous app state if one exists. if let Some(storage) = cc.storage { this.config = eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default() } this } fn advance_state_by_dt(&mut self, interactive_input: &InteractiveInput) { self.update_counter += interactive_input.dt; while self.update_counter >= self.menu_bar.frame_timestep { self.update_counter -= self.menu_bar.frame_timestep; if let ControlFlow::Break(()) = self.update_state(interactive_input) { return; } } } fn update_state(&mut self, interactive_input: &InteractiveInput) -> ControlFlow<()> { if let Ok(data) = self.content_channel.1.try_recv() { match data { Ok(design) => match Workspace::new(design, &self.translator) { Ok(ws) => { self.maybe_workspace = Some(ws); self.viewport.scheduled_zoom_to_fit = true; } Err(err) => { self.error_dialog .push_error("tr-module-specctra-dsn-file-loader", err); } }, Err(SpecctraLoadingError::Parse(err)) => { self.error_dialog.push_error( "tr-module-specctra-dsn-file-loader", format!( "{}; {}", self.translator .text("tr-error-failed-to-parse-as-specctra-dsn"), err ), ); } Err(SpecctraLoadingError::Io(err)) => { self.error_dialog.push_error( "tr-module-specctra-dsn-file-loader", format!( "{}; {}", self.translator.text("tr-error-unable-to-read-file"), err ), ); } } } if let Some(workspace) = &mut self.maybe_workspace { return workspace.update_state( &self.translator, &mut self.error_dialog, interactive_input, ); } ControlFlow::Break(()) } #[cfg(not(target_arch = "wasm32"))] fn update_locale(&mut self) { // I don't know any equivalent of changing the lang property in desktop. } #[cfg(target_arch = "wasm32")] fn update_locale(&mut self) { use eframe::wasm_bindgen::JsCast; let document_element = eframe::web_sys::window() .expect("No window") .document() .expect("No document") .document_element() .expect("No document element"); document_element.set_attribute("lang", &self.translator.langid().to_string()); } } impl eframe::App for App { /// Called to save state before shutdown. fn save(&mut self, storage: &mut dyn eframe::Storage) { eframe::set_value(storage, eframe::APP_KEY, &self.config); } /// Called each time the UI has to be repainted. fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { self.menu_bar.update( ctx, &mut self.translator, self.content_channel.0.clone(), &mut self.viewport, self.maybe_workspace.as_mut(), ); let pointer_pos = self.viewport.transform.inverse() * ctx.input(|i| i.pointer.latest_pos().unwrap_or_default()); self.advance_state_by_dt(&InteractiveInput { pointer_pos: point! {x: pointer_pos.x as f64, y: pointer_pos.y as f64}, dt: ctx.input(|i| i.stable_dt), }); self.status_bar.update( ctx, &self.translator, &self.viewport, self.maybe_workspace .as_ref() .and_then(|w| w.interactor.maybe_activity().as_ref()), ); if self.menu_bar.show_layer_manager { if let Some(workspace) = &mut self.maybe_workspace { workspace.update_layers(ctx); } } self.error_dialog.update(ctx, &self.translator); let _viewport_rect = self.viewport .update(ctx, &self.menu_bar, self.maybe_workspace.as_mut()); self.update_locale(); if ctx.input(|i| i.key_pressed(egui::Key::Escape)) { ctx.send_viewport_cmd(egui::ViewportCommand::Close); } } } #[cfg(not(target_arch = "wasm32"))] pub fn execute + Send + 'static>(f: F) { std::thread::spawn(move || futures_lite::future::block_on(f)); } #[cfg(target_arch = "wasm32")] pub fn execute + 'static>(f: F) { wasm_bindgen_futures::spawn_local(f); } pub async fn handle_file(file_handle: &rfd::FileHandle) -> io::Result { #[cfg(not(target_arch = "wasm32"))] let res = io::BufReader::new(std::fs::File::open(file_handle.path())?); #[cfg(target_arch = "wasm32")] let res = io::Cursor::new(file_handle.read().await); Ok(res) }