mirror of https://codeberg.org/topola/topola.git
221 lines
6.8 KiB
Rust
221 lines
6.8 KiB
Rust
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<Result<SpecctraDesign, SpecctraLoadingError>>,
|
|
Receiver<Result<SpecctraDesign, SpecctraLoadingError>>,
|
|
),
|
|
|
|
viewport: Viewport,
|
|
menu_bar: MenuBar,
|
|
status_bar: StatusBar,
|
|
error_dialog: ErrorDialog,
|
|
|
|
maybe_workspace: Option<Workspace>,
|
|
|
|
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<F: Future<Output = ()> + Send + 'static>(f: F) {
|
|
std::thread::spawn(move || futures_lite::future::block_on(f));
|
|
}
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
pub fn execute<F: Future<Output = ()> + 'static>(f: F) {
|
|
wasm_bindgen_futures::spawn_local(f);
|
|
}
|
|
|
|
pub async fn handle_file(file_handle: &rfd::FileHandle) -> io::Result<impl io::BufRead + io::Seek> {
|
|
#[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)
|
|
}
|