diff --git a/.gitignore b/.gitignore index 61dd8fe..b5440cc 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ **/dist/* .vscode/ **/*.wb +.cargo/config.toml \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 24aa2c2..963ec77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [v1.3.0] + +### Added +- Feature Request #34: Save preferences +- Feature Request #35: Add a survey pop-up + +### Fixed +- Issue #6: Uploading and downloading files in WASM +- Issue #30: Letters cut-off by scrollbar +- Issue #31: Composition help unclear +- Issue #33: Explain that you cannot compose more than one library at a time + ## [v1.2.2] ### Added - Help for the composition window @@ -104,4 +116,4 @@ - Fixed final state's instruction needing to be explicitly defined (#2) ## [v0.2.0] - 2022-11-21 -- Initial version \ No newline at end of file +- Initial version diff --git a/Cargo.lock b/Cargo.lock index a2429f9..8aaf269 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -906,6 +906,27 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "directories" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "dispatch" version = "0.2.0" @@ -2217,13 +2238,19 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "orbclient" version = "0.3.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "221d488cd70617f1bd599ed8ceb659df2147d9393717954d82a0f5e8032a6ab1" dependencies = [ - "redox_syscall", + "redox_syscall 0.3.5", ] [[package]] @@ -2287,7 +2314,7 @@ checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.3.5", "smallvec", "windows-targets 0.48.1", ] @@ -2379,6 +2406,17 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "poll-promise" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf2a02372dfae23c9c01267fb296b8a3413bb4e45fbd589c3ac73c6dcfbb305" +dependencies = [ + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", +] + [[package]] name = "polling" version = "2.8.0" @@ -2395,6 +2433,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "pollster" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -2477,6 +2521,15 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_syscall" version = "0.3.5" @@ -2486,6 +2539,17 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall 0.2.16", + "thiserror", +] + [[package]] name = "regex" version = "1.9.1" @@ -2897,7 +2961,7 @@ checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998" dependencies = [ "cfg-if", "fastrand 2.0.0", - "redox_syscall", + "redox_syscall 0.3.5", "rustix 0.38.4", "windows-sys 0.48.0", ] @@ -3105,13 +3169,14 @@ dependencies = [ [[package]] name = "turing-machine" -version = "1.2.2" +version = "1.3.0" dependencies = [ "base64", "bincode", "clap", "clap-verbosity-flag", "console_error_panic_hook", + "directories", "eframe", "egui_extras", "env_logger", @@ -3122,13 +3187,17 @@ dependencies = [ "log", "pest", "pest_derive", + "poll-promise", + "pollster", "rfd", "serde", "serde_bytes", "sys-locale", + "toml", "tracing-subscriber", "tracing-wasm", "turing-lib", + "version", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -3220,6 +3289,14 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" +[[package]] +name = "version" +version = "0.1.2" +source = "git+https://github.com/turing-marcos/rs-version.git?tag=v0.1.2#8c8b266acfab3081d558af5e33a3c051e760eff4" +dependencies = [ + "serde", +] + [[package]] name = "version-compare" version = "0.1.1" @@ -3764,7 +3841,7 @@ dependencies = [ "orbclient", "percent-encoding", "raw-window-handle", - "redox_syscall", + "redox_syscall 0.3.5", "sctk-adwaita", "smithay-client-toolkit", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index 3f4a61e..0595427 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "turing-machine" -version = "1.2.2" +version = "1.3.0" edition = "2021" authors = ["Marcos Gutiérrez Alonso "] description = "Turing Machine Simulator" @@ -10,7 +10,6 @@ license = "GPL-2.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -clap = { version = "^4.3", features = ["derive"] } pest = "^2.7" pest_derive = "^2.4" eframe = {version = "^0.22", features = ["wayland"]} @@ -23,16 +22,22 @@ turing-lib = "^2.1" serde = {version = "^1.0", features = ["derive"]} serde_bytes = "0.11" bincode = "1.3" -log = "^0.4" -env_logger = "^0.10" sys-locale = "^0.3" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] +toml = "0.7.6" +directories = "^5.0" +log = "^0.4" +env_logger = "^0.10" tracing-subscriber = "0.3" +clap = { version = "^4.3", features = ["derive"] } clap-verbosity-flag = "2.0.0" +version = {git = "https://github.com/turing-marcos/rs-version.git", tag = "v0.1.2"} # web: [target.'cfg(target_arch = "wasm32")'.dependencies] +poll-promise = { version = "0.2.0", features = ["web"] } +pollster = "0.3.0" console_error_panic_hook = "^0.1" tracing-wasm = "^0.2" wasm-bindgen = "^0.2" diff --git a/assets/save_file.js b/assets/save_file.js deleted file mode 100644 index a3400a2..0000000 --- a/assets/save_file.js +++ /dev/null @@ -1,9 +0,0 @@ -export function saveFile(filename, content) { - const blob = new Blob([content], { type: "text/plain;charset=utf-8" }); - const url = URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = url; - link.download = filename; - link.click(); - URL.revokeObjectURL(url); -} \ No newline at end of file diff --git a/assets/utils.js b/assets/utils.js new file mode 100644 index 0000000..17d999c --- /dev/null +++ b/assets/utils.js @@ -0,0 +1,10 @@ +export function downloadToFile(content, filename) { + const a = document.createElement('a'); + const file = new Blob([content], { type: 'text/plain'}); + + a.href = URL.createObjectURL(file); + a.download = filename; + a.click(); + + URL.revokeObjectURL(a.href); +}; diff --git a/index.html b/index.html index f5f0496..eed2bbf 100644 --- a/index.html +++ b/index.html @@ -20,7 +20,6 @@ - diff --git a/locales/main_window.json b/locales/main_window.json index fff603c..26960bb 100644 --- a/locales/main_window.json +++ b/locales/main_window.json @@ -134,6 +134,22 @@ "btn.close": { "en": "Close", "es": "Cerrar" + }, + "window.title.survey": { + "en": "Survey", + "es": "Encuesta" + }, + "window.link.survey": { + "en": "Click here to open the survey", + "es": "Haz clic aquí para abrir la encuesta" + }, + "lbl.window.survey":{ + "en": "Please take a moment to fill out this survey to help us improve the app.", + "es": "Por favor, tómate un momento para completar esta encuesta y ayudarnos a mejorar la aplicación." + }, + "lbl.window.survey2": { + "en": "It will only take a few minutes, I promise!", + "es": "¡Sólo te llevará unos minutos, lo prometo!" } } - \ No newline at end of file + diff --git a/locales/windows/composition_window.json b/locales/windows/composition_window.json index e958faf..536d249 100644 --- a/locales/windows/composition_window.json +++ b/locales/windows/composition_window.json @@ -18,5 +18,9 @@ "lbl.composition.help.txt": { "en": "Type `compose = { };` before the state definitions to use a library", "es": "Escribe `compose = { };` antes de definir los estados para usar una librería" + }, + "lbl.composition.warning": { + "en": "WARNING: Composing more than one library will break all compositions.", + "es": "ATENCIÓN: Componer más de una librería romperá todas las composiciones." } -} \ No newline at end of file +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..5a646de --- /dev/null +++ b/src/config.rs @@ -0,0 +1,206 @@ +use std::{fs::File, io::Write}; + +use directories::ProjectDirs; +use log::{error, info}; + +use serde::{Deserialize, Serialize}; +use toml; +use version::Version; + +use crate::{get_lang, Language}; + +const QUALIFIER: &str = "org"; +const ORGANIZATION: &str = "margual56"; +const APPLICATION: &str = "Turing Machine"; + +#[derive(Serialize, Deserialize, Copy, Clone, Debug)] +pub struct Config { + version: Version, + + pub times_opened: u32, + + pub language: Language, + + /// Autosave is enabled by default. Has the user manually disabled it? + autosave_disabled: bool, + + pub tape_size: f32, + + pub tape_speed: f32, + + pub threshold_inf_loop: usize, + + pub served_survey: bool, +} + +impl Config { + pub fn default() -> Self { + Config { + version: Version::get().unwrap(), + times_opened: 0, + language: get_lang(), + autosave_disabled: false, + tape_size: 100.0, + tape_speed: 1.0, + threshold_inf_loop: 100, + served_survey: false, + } + } + + pub fn load() -> Option { + match ProjectDirs::from( + QUALIFIER, /*qualifier*/ + ORGANIZATION, /*organization*/ + APPLICATION, /*application*/ + ) { + Some(dir) => { + let file = dir.config_dir().join("config.toml"); + + log::info!("Loading configuration file: {:?}", file); + + match toml::from_str::( + &(match std::fs::read_to_string(&file) { + Ok(s) => s, + Err(e) => { + error!("Cannot read configuration file: {}", e); + return None; + } + }), + ) { + Ok(c) => { + let mut c = c; + c.increment_launches(); + log::info!("Incremented launches: {}", c.times_opened); + + if !Version::get().unwrap().is_compatible_with(&c.version) { + log::error!("A new version of the program is being used! Resetting configuration (it may be incompatible)..."); + + std::fs::remove_file(&file).unwrap(); + let new_c = Config::default(); + new_c.save(); + return Some(new_c); + } + + Some(c) + } + Err(e) => { + error!("Cannot parse configuration file: {}", e); + None + } + } + } + None => { + error!("Cannot find a valid directory to store the configuration file."); + return None; + } + } + } + + pub fn save(&self) { + match ProjectDirs::from( + QUALIFIER, /*qualifier*/ + ORGANIZATION, /*organization*/ + APPLICATION, /*application*/ + ) { + Some(dir) => { + if !dir.config_dir().try_exists().unwrap_or(true) { + match std::fs::create_dir_all(dir.config_dir()) { + Ok(_) => { + info!("Created configuration directory: {:?}", dir.config_dir()) + } + Err(e) => { + error!( + "Could not create configuration directory {:?}: {}", + dir.config_dir(), + e + ); + return; + } + }; + } + + let file_path = dir.config_dir().join("config.toml"); + + let mut file: File = match File::create(&file_path) { + Ok(f) => f, + Err(e) => { + error!( + "Could not create configuration file {}: {}", + &file_path.to_string_lossy(), + e + ); + return; + } + }; + + log::info!( + "Writing to configuration file: {}", + &file_path.to_string_lossy() + ); + + let serialized_config = toml::to_string_pretty(self).unwrap(); + + match file.write_all(serialized_config.as_bytes()) { + Ok(_) => {} + Err(e) => error!("Could not write configuration file: {}", e), + }; + } + None => {} + }; + } + + pub fn language(&self) -> Language { + self.language + } + + pub fn set_language(&mut self, l: Language) { + self.language = l; + self.save(); + } + + pub fn autosave_disabled(&self) -> bool { + self.autosave_disabled + } + + pub fn set_autosave_disabled(&mut self, b: bool) { + self.autosave_disabled = b; + self.save(); + } + + pub fn threshold_inf_loop(&self) -> usize { + self.threshold_inf_loop + } + + pub fn set_threshold_inf_loop(&mut self, t: usize) { + self.threshold_inf_loop = t; + self.save(); + } + + pub fn tape_size(&self) -> f32 { + self.tape_size + } + + pub fn set_tape_size(&mut self, s: f32) { + self.tape_size = s; + self.save(); + } + + pub fn tape_speed(&self) -> f32 { + self.tape_speed + } + + pub fn set_tape_speed(&mut self, s: f32) { + self.tape_speed = s; + self.save(); + } + + pub fn increment_launches(&mut self) { + self.times_opened += 1; + self.save(); + } + + pub fn survey_served(&mut self) { + self.served_survey = true; + self.save(); + } +} diff --git a/src/lib.rs b/src/lib.rs index b3ef96e..a389214 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +#[cfg(not(target_family = "wasm"))] +mod config; mod turing_widget; mod window; pub mod windows; @@ -5,6 +7,9 @@ pub mod windows; pub use turing_widget::TuringWidget; pub use window::{Language, MyApp}; +#[cfg(not(target_family = "wasm"))] +pub use config::Config; + pub fn get_lang() -> Language { match sys_locale::get_locale() { Some(locale) => { @@ -46,6 +51,11 @@ extern "C" { #[wasm_bindgen(js_namespace = console, js_name = log)] fn log_many(a: &str, b: &str); + // Use `js_namespace` here to bind `console.warn(..)` instead of just + // `warn(..)` + #[wasm_bindgen(js_namespace = console)] + fn warn(s: &str); + // Use `js_namespace` here to bind `console.err(..)` instead of just // `log(..)` #[wasm_bindgen(js_namespace = console)] @@ -77,3 +87,11 @@ macro_rules! console_err { // `bare_bones` ($($t:tt)*) => (crate::err(&format_args!($($t)*).to_string())) } + +#[cfg(target_family = "wasm")] +#[macro_export] +macro_rules! console_warn { + // Note that this is using the `err` function imported above during + // `bare_bones` + ($($t:tt)*) => (crate::warn(&format_args!($($t)*).to_string())) +} diff --git a/src/turing_widget.rs b/src/turing_widget.rs index 241b5c4..c19769a 100644 --- a/src/turing_widget.rs +++ b/src/turing_widget.rs @@ -3,9 +3,14 @@ use eframe::emath::Align2; use eframe::epaint::{Color32, FontFamily, FontId, Pos2, Rect, Rounding, Stroke, Vec2}; use internationalization::t; +#[cfg(not(target_family = "wasm"))] use log::warn; + use turing_lib::{CompilerError, CompilerWarning, Library, TuringMachine, TuringOutput}; +#[cfg(target_family = "wasm")] +use crate::console_warn; + use crate::window::is_mobile; const STROKE_WIDTH: f32 = 3f32; @@ -60,12 +65,28 @@ impl TuringWidget { } } + #[cfg(not(target_family = "wasm"))] + pub fn set_config(&self, config: &crate::config::Config) -> Self { + let mut new_tm = self.clone(); + + new_tm.lang = config.language().to_string(); + new_tm.threshold_inf_loop = config.threshold_inf_loop(); + new_tm.tape_rect_size = config.tape_size(); + new_tm.tape_anim_speed = config.tape_speed(); + + new_tm + } + /// Restarts the turing machine with the given code pub fn restart(&mut self, code: &str) -> Result { let (tm, warnings) = match TuringMachine::new(code) { Ok((t, warnings)) => { for w in &warnings { + #[cfg(not(target_family = "wasm"))] warn!("Compiler warning: {:?}", w); + + #[cfg(target_family = "wasm")] + console_warn!("Compiler warning: {:?}", w); } self.errors = None; diff --git a/src/window.rs b/src/window.rs index d185549..3465aaa 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,12 +1,10 @@ use std::{ - fs::{self, File}, - io::Write, + fmt::Display, path::PathBuf, time::{Duration, Instant}, }; use crate::{ - get_lang, windows::{ AboutWindow, CompositionHelpWindow, DebugWindow, InfiniteLoopWindow, SecondaryWindow, WorkbookEditorWindow, WorkbookWindow, @@ -17,15 +15,25 @@ use eframe; use eframe::egui::{self, Id, RichText, TextEdit, Ui}; use eframe::epaint::Color32; use internationalization::t; -use log::{debug, error, info, trace, warn}; use turing_lib::TuringOutput; use turing_lib::{CompilerError, TuringMachine}; -#[cfg(target_arch = "wasm32")] +#[cfg(not(target_family = "wasm"))] use { - //wasm_bindgen::prelude::wasm_bindgen, - crate::{console_err, console_log}, - std::sync::{Arc, Mutex}, + crate::Config, + log::{debug, error, info, trace, warn}, + serde::{Deserialize, Serialize}, + std::{ + fs::{self, File}, + io::Write, + }, +}; + +#[cfg(target_family = "wasm")] +use { + crate::{console_err, console_log, console_warn, get_lang}, + poll_promise::Promise, + wasm_bindgen::prelude::wasm_bindgen, }; const DEFAULT_CODE: &str = include_str!("../Examples/Example1.tm"); @@ -35,19 +43,39 @@ pub fn is_mobile(ctx: &egui::Context) -> bool { ctx.screen_rect().width() < MOBILE_THRESHOLD } -// Import the saveFile function -//#[cfg(target_arch = "wasm32")] -//#[wasm_bindgen(module = "/dist/.stage/save_file.js")] -//extern "C" { -// fn saveFile(filename: &str, content: &str); -//} +#[cfg(target_family = "wasm")] +#[wasm_bindgen(module = "/assets/utils.js")] +extern "C" { + fn downloadToFile(content: &str, filename: &str); +} + +#[cfg(not(target_family = "wasm"))] +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum Language { + English, + Spanish, +} +#[cfg(target_family = "wasm")] #[derive(Copy, Clone, Debug, PartialEq)] pub enum Language { English, Spanish, } +impl Display for Language { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Language::English => "en", + Language::Spanish => "es", + } + ) + } +} + pub struct MyApp { code: String, error: Option, @@ -61,11 +89,20 @@ pub struct MyApp { workbook_editor_window: Option>, composition_help_window: Option>, - lang: Language, - - file: Option, + #[cfg(not(target_family = "wasm"))] + config: Config, + #[cfg(not(target_family = "wasm"))] autosave: bool, + saved_feedback: Option, + + file: Option, + + #[cfg(target_family = "wasm")] + lang: Language, + + #[cfg(target_family = "wasm")] + file_request_future: Option>>, } impl MyApp { @@ -73,25 +110,47 @@ impl MyApp { file: &Option, cc: &eframe::CreationContext<'_>, ) -> Result { - let code = match file { - Some(ref f) => { - trace!("File provided: {:?}", file); - let unparsed_file = fs::read_to_string(&f).expect("cannot read file"); - unparsed_file - } - None => { - trace!("No file provided, opening an example"); - DEFAULT_CODE.to_string() + let code = if cfg!(target_family = "wasm") { + DEFAULT_CODE.to_string() + } else { + match file { + Some(ref f) => { + #[cfg(not(target_family = "wasm"))] + { + trace!("File provided: {:?}", file); + + let unparsed_file = fs::read_to_string(&f).expect("cannot read file"); + unparsed_file + } + + #[cfg(target_family = "wasm")] + DEFAULT_CODE.to_string() + } + None => { + #[cfg(not(target_family = "wasm"))] + trace!("No file provided, opening an example"); + + DEFAULT_CODE.to_string() + } } }; let (tm, warnings) = match TuringMachine::new(&code) { Ok((t, warnings)) => { for w in &warnings { + #[cfg(not(target_family = "wasm"))] warn!("\tCompiler warning: {:?}", w); + + #[cfg(target_family = "wasm")] + console_warn!("\tCompiler warning: {:?}", w) } + #[cfg(not(target_family = "wasm"))] trace!("Turing machine created successfully"); + + #[cfg(target_family = "wasm")] + console_log!("Turing machine created successfully"); + (t, warnings) } Err(e) => { @@ -106,29 +165,72 @@ impl MyApp { st.spacing.item_spacing = egui::Vec2::new(10.0, 10.0); cc.egui_ctx.set_style(st); - Ok(Self { - code: String::from(&tm.code), - error: None, - tm: TuringWidget::new(tm, warnings), - about_window: None, - debug_window: None, - infinite_loop_window: None, - book_window: None, - workbook_editor_window: None, - composition_help_window: None, - - lang: get_lang(), - - file: file.clone(), - autosave: file.is_some(), - saved_feedback: None, - }) + #[cfg(not(target_family = "wasm"))] + { + let config = match Config::load() { + Some(c) => c, + None => { + debug!("The config file did not exist, creating a default one"); + + let c = Config::default(); + + c.save(); + + c + } + }; + + Ok(Self { + code: String::from(&tm.code), + error: None, + tm: TuringWidget::new(tm, warnings).set_config(&config), + about_window: None, + debug_window: None, + infinite_loop_window: None, + book_window: None, + workbook_editor_window: None, + composition_help_window: None, + + config, + autosave: file.is_some() && config.autosave_disabled(), + + file: file.clone(), + saved_feedback: None, + }) + } + + #[cfg(target_family = "wasm")] + { + Ok(Self { + code: String::from(&tm.code), + error: None, + tm: TuringWidget::new(tm, warnings), + about_window: None, + debug_window: None, + infinite_loop_window: None, + book_window: None, + workbook_editor_window: None, + composition_help_window: None, + + lang: get_lang(), + + file: file.clone(), + saved_feedback: None, + + file_request_future: None, + }) + } } pub fn get_lang(&self) -> String { - match self.lang { - Language::English => String::from("en"), - Language::Spanish => String::from("es"), + #[cfg(not(target_family = "wasm"))] + { + self.config.language().to_string() + } + + #[cfg(target_family = "wasm")] + { + self.lang.to_string() } } @@ -290,6 +392,7 @@ impl MyApp { /// # Returns /// An Option representing the time the file was saved if the save operation is successful, /// or None if the save operation fails or if there is no associated file. + #[cfg(not(target_family = "wasm"))] fn auto_save_file(&self) -> Option { if let Some(file) = &self.file { if let Ok(mut file) = File::create(file) { @@ -384,38 +487,29 @@ impl MyApp { /// /// After a successful save operation, the file is set as the new auto-save file. If the save operation /// fails, an error message is logged. + #[cfg(not(target_family = "wasm"))] fn save_file(&mut self) { - #[cfg(target_family = "wasm")] - { - let filename = "exercise.tm"; // Replace with your desired file name - let content = &self.code; - //saveFile(filename, content); - } - - #[cfg(not(target_family = "wasm"))] - { - let file: Option = match &self.file { - Some(f) => Some(f.clone()), - None => { - let path = std::env::current_dir().unwrap(); + let file: Option = match &self.file { + Some(f) => Some(f.clone()), + None => { + let path = std::env::current_dir().unwrap(); - rfd::FileDialog::new() - .add_filter("TuringMachine", &["tm"]) - .set_directory(&path) - .save_file() - } - }; + rfd::FileDialog::new() + .add_filter("TuringMachine", &["tm"]) + .set_directory(&path) + .save_file() + } + }; - if let Some(f) = file { - std::fs::write(&f, self.code.as_bytes()).expect("cannot write file"); - self.file = Some(f); + if let Some(f) = file { + std::fs::write(&f, self.code.as_bytes()).expect("cannot write file"); + self.file = Some(f); - debug!("Set auto-save file to {:?}", self.file); + debug!("Set auto-save file to {:?}", self.file); - self.saved_feedback = Some(Instant::now()); - } else { - error!("Cannot save file"); - } + self.saved_feedback = Some(Instant::now()); + } else { + error!("Cannot save file"); } } @@ -430,15 +524,8 @@ impl MyApp { /// /// After a successful save operation, the file is set as the new auto-save file. If the save operation /// fails, an error message is logged. + #[cfg(not(target_family = "wasm"))] fn save_file_as(&mut self) { - #[cfg(target_family = "wasm")] - { - let filename = "exercise.tm"; // Replace with your desired file name - let content = &self.code; - //saveFile(filename, content); - } - - #[cfg(not(target_family = "wasm"))] { let path = std::env::current_dir().unwrap(); @@ -460,51 +547,6 @@ impl MyApp { } } - #[cfg(target_family = "wasm")] - fn clone_for_load_file(&self) -> Self { - MyApp { - tm: self.tm.clone(), - code: self.code.clone(), - error: self.error.clone(), - about_window: None, - debug_window: None, - infinite_loop_window: None, - book_window: None, - workbook_editor_window: None, - composition_help_window: None, - lang: self.lang.clone(), - - file: self.file.clone(), - autosave: self.autosave.clone(), - saved_feedback: None, - } - } - - #[cfg(target_family = "wasm")] - async fn load_file_async() -> Option { - let file = rfd::AsyncFileDialog::new().pick_file().await; - - if let Some(f) = file { - // If you care about wasm support you just read() the file - let buffer = f.read().await; - console_log!("Read file: {:?}", buffer); - match String::from_utf8(buffer) { - Ok(s) => { - console_log!("Correctly parsed to utf-8: {}", s); - - Some(s) - } - Err(e) => { - console_err!("Invalid UTF-8 sequence: {}", e); - - None - } - } - } else { - None - } - } - /// This method loads the code from an associated file, or spawns a dialog to select a file and then /// loads the code from it. The method handles both WebAssembly and non-WebAssembly targets. /// @@ -522,28 +564,31 @@ impl MyApp { /// the Turing machine state remains unchanged. If no file is selected or the file dialog operation /// fails, the method does nothing. #[cfg(target_family = "wasm")] - async fn load_file(&mut self) { - // Spawn future without boxing it - let code = Self::load_file_async().await; + async fn load_file() -> Option { + let res = rfd::AsyncFileDialog::new() + .add_filter("TuringMachine", &["tm"]) + .pick_file() + .await; - let new_tm = match self.tm.restart(code.as_ref().unwrap()) { - Ok(t) => { - console_log!("Correctly created the new turing machine with the given code"); - self.error = None; - t + match res { + Some(file) => { + console_log!("The file {:?} has been loaded, reading...", file); + + let data: Vec = file.read().await; + + match String::from_utf8(data) { + Ok(s) => Some(s), + Err(e) => { + console_err!("Error reading file: {:?}", e); + None + } + } } - Err(e) => { - console_err!( - "Error creating the new turing machine with the given code: {:?}", - e - ); - self.error = Some(e); - self.tm.clone() + None => { + console_err!("Error selecting file"); + None } - }; - console_log!("Gathering results..."); - self.code = String::from(code.as_ref().unwrap()); - self.tm = new_tm; + } } #[cfg(not(target_family = "wasm"))] @@ -581,6 +626,29 @@ impl MyApp { /// * ctx - An egui::Context object required for creating and displaying UI components. /// * lang - A string representing the language used for displaying text in the UI. fn handle_windows(&mut self, ctx: &egui::Context, lang: &str) { + #[cfg(not(target_family = "wasm"))] + if self.config.times_opened >= 2 && !self.config.served_survey { + egui::Window::new(t!("window.title.survey", lang)) + .collapsible(false) + .default_pos([ + ctx.screen_rect().width() / 2.0 - 400.0, + ctx.screen_rect().height() / 2.0 - 200.0, + ]) + .show(ctx, |ui| { + ui.label(t!("lbl.window.survey", lang)); + + if ui.link(t!("window.link.survey", lang)).clicked() { + webbrowser::open( + "https://next.coldboard.net/apps/forms/s/5HYog9PptWdx458y6F2LJNGL", + ) + .unwrap(); + self.config.survey_served(); + } + + ui.label(t!("lbl.window.survey2", lang)); + }); + } + if let Some(about) = &self.about_window { if !about.show(ctx) { self.about_window = None; @@ -661,24 +729,9 @@ impl MyApp { { #[cfg(target_family = "wasm")] { - // Call the function load file with `&mut self` and await it on the main thread - let shared_self = - Arc::new(Mutex::new(self.clone_for_load_file())); - let shared_self_clone = Arc::clone(&shared_self); - let future = async move { - let mut shared_self = shared_self_clone.lock().unwrap(); - shared_self.load_file().await; - }; - wasm_bindgen_futures::spawn_local(future); - // Wait for the result - let shared_self = shared_self.lock().unwrap(); - self.tm = shared_self.tm.clone(); - self.code = shared_self.code.clone(); - - console_log!("Retrieved code: {}", self.code); - - self.error = shared_self.error.clone(); - self.file = shared_self.file.clone(); + self.file_request_future = Some( + poll_promise::Promise::spawn_async(Self::load_file()), + ); } #[cfg(not(target_family = "wasm"))] @@ -689,7 +742,11 @@ impl MyApp { .add(egui::Button::new("Save").shortcut_text("Ctrl + S")) .clicked() { + #[cfg(not(target_family = "wasm"))] self.save_file(); + + #[cfg(target_family = "wasm")] + downloadToFile(&self.code, "my-turing-program.tm"); } if ui @@ -699,11 +756,21 @@ impl MyApp { ) .clicked() { + #[cfg(not(target_family = "wasm"))] self.save_file_as(); + + #[cfg(target_family = "wasm")] + downloadToFile(&self.code, "my-turing-program.tm"); } + #[cfg(not(target_family = "wasm"))] ui.add_enabled_ui(self.file.is_some(), |ui| { - ui.checkbox(&mut self.autosave, "Autosave") + let prev = self.autosave.clone(); + ui.checkbox(&mut self.autosave, "Autosave"); + + if prev != self.autosave { + self.config.set_autosave_disabled(self.autosave); + } }); }); @@ -745,16 +812,36 @@ impl MyApp { } ui.menu_button(t!("menu.language", lang), |ui| { - ui.radio_value( - &mut self.lang, - Language::English, - t!("lang.en", lang), - ); - ui.radio_value::( - &mut self.lang, - Language::Spanish, - t!("lang.es", lang), - ); + #[cfg(target_family = "wasm")] + { + ui.radio_value( + &mut self.lang, + Language::English, + t!("lang.en", lang), + ); + ui.radio_value::( + &mut self.lang, + Language::Spanish, + t!("lang.es", lang), + ); + } + #[cfg(not(target_family = "wasm"))] + { + ui.radio_value( + &mut self.config.language, + Language::English, + t!("lang.en", lang), + ); + ui.radio_value::( + &mut self.config.language, + Language::Spanish, + t!("lang.es", lang), + ); + + if self.config.language.to_string() != lang { + self.config.save(); + } + } }); ui.menu_button(t!("menu.about", lang), |ui| { @@ -771,6 +858,16 @@ impl MyApp { ) .unwrap(); } + + if ui.link(t!("window.title.survey", lang)).clicked() { + webbrowser::open( + "https://next.coldboard.net/apps/forms/s/5HYog9PptWdx458y6F2LJNGL", + ) + .unwrap(); + + #[cfg(not(target_family = "wasm"))] + self.config.survey_served(); + } }); }); }); @@ -810,23 +907,8 @@ impl MyApp { { #[cfg(target_family = "wasm")] { - // Call the function load file with `&mut self` and await it on the main thread - let shared_self = Arc::new(Mutex::new(self.clone_for_load_file())); - let shared_self_clone = Arc::clone(&shared_self); - let future = async move { - let mut shared_self = shared_self_clone.lock().unwrap(); - shared_self.load_file().await; - }; - wasm_bindgen_futures::spawn_local(future); - // Wait for the result - let shared_self = shared_self.lock().unwrap(); - self.tm = shared_self.tm.clone(); - self.code = shared_self.code.clone(); - - console_log!("Retrieved code: {}", self.code); - - self.error = shared_self.error.clone(); - self.file = shared_self.file.clone(); + self.file_request_future = + Some(poll_promise::Promise::spawn_async(Self::load_file())); } #[cfg(not(target_family = "wasm"))] @@ -842,7 +924,11 @@ impl MyApp { ) .clicked() { + #[cfg(not(target_family = "wasm"))] self.save_file(); + + #[cfg(target_family = "wasm")] + downloadToFile(&self.code, "my-turing-program.tm"); } }); @@ -910,16 +996,33 @@ impl MyApp { .show(ui, |my_ui: &mut Ui| { let editor = TextEdit::multiline(&mut self.code) .code_editor() - .desired_width(0.0); + .desired_width(0.0) + .show(my_ui); - let res = my_ui.add(editor); + let res = editor.response; + // Autosave only works on desktop + #[cfg(not(target_family = "wasm"))] if self.autosave && res.lost_focus() { debug!("Saving file"); + self.saved_feedback = self.auto_save_file(); } - *editor_focused = res.has_focus().clone(); + *editor_focused = (&res).has_focus().clone(); + + // FIXME: Does not work because TextEdit is lacking the Sense(click) + // res.context_menu(|ui| { + // if ui.button("Copy").clicked() { + // if let Some(cursor_range) = editor.cursor_range { + // let start = cursor_range.primary.ccursor.index; + // let end = cursor_range.secondary.ccursor.index; + + // let text = &self.code[start..end]; + // ui.output_mut(|o| o.copied_text = String::from(text)); + // } + // } + // }); }); if ui.button(t!("btn.libraries", lang)).clicked() { @@ -928,7 +1031,9 @@ impl MyApp { } if self.saved_feedback.is_some() { + #[cfg(not(target_family = "wasm"))] debug!("Drawing saved feedback popup"); + self.draw_saved_feedback_popup(ui, ctx); } }) @@ -973,6 +1078,10 @@ impl MyApp { } let mut sliders = |ui: &mut egui::Ui| { + let _prev_tape_size = self.tm.tape_rect_size; + let _prev_tape_speed = self.tm.tape_anim_speed; + let _prev_threshold_inf_loop = self.tm.threshold_inf_loop; + ui.add( egui::Slider::new(&mut self.tm.tape_rect_size, 25.0..=300.0) .suffix(" px") @@ -991,6 +1100,22 @@ impl MyApp { .text(t!("lbl.tape.inf_loop", lang)), ) .on_hover_text_at_pointer(t!("tooltip.tape.iterations", lang)); + + #[cfg(not(target_family = "wasm"))] + { + if _prev_tape_size != self.tm.tape_rect_size { + self.config.set_tape_size(self.tm.tape_rect_size); + } + + if _prev_tape_speed != self.tm.tape_anim_speed { + self.config.set_tape_speed(self.tm.tape_anim_speed); + } + + if _prev_threshold_inf_loop != self.tm.threshold_inf_loop { + self.config + .set_threshold_inf_loop(self.tm.threshold_inf_loop); + } + } }; if is_mobile(ctx) { @@ -1062,7 +1187,12 @@ impl MyApp { { ctx.request_repaint(); if self.tm.is_inf_loop() { + #[cfg(not(target_family = "wasm"))] warn!("Infinite loop detected!"); + + #[cfg(target_family = "wasm")] + console_warn!("Infinite loop detected!"); + self.infinite_loop_window = Some(Box::new( InfiniteLoopWindow::new(&self.get_lang()), )); @@ -1091,6 +1221,27 @@ impl eframe::App for MyApp { let lang = self.get_lang(); let mut editor_focused = false; + #[cfg(target_family = "wasm")] + if let Some(file_async) = &self.file_request_future { + if let Some(file_result) = file_async.ready() { + if let Some(new_code) = file_result { + self.tm = match self.tm.restart(new_code) { + Ok(t) => { + self.error = None; + t + } + Err(e) => { + self.error = Some(e); + self.tm.clone() + } + }; + self.code = String::from(new_code); + } + + self.file_request_future = None; + } + } + ctx.input_mut(|i| { // Check for keyboard shortcuts if i.consume_shortcut(&egui::KeyboardShortcut::new( @@ -1098,37 +1249,34 @@ impl eframe::App for MyApp { egui::Key::S, )) { // Ctrl+S - debug!("Saving..."); - self.save_file(); + #[cfg(not(target_family = "wasm"))] + { + debug!("Saving..."); + self.save_file(); + } + #[cfg(target_family = "wasm")] + { + console_log!("Saving..."); + downloadToFile(&self.code, "my-turing-program.tm"); + } } else if i.consume_shortcut(&egui::KeyboardShortcut::new( egui::Modifiers::COMMAND, egui::Key::O, )) { // Ctrl+O - debug!("Opening..."); #[cfg(target_family = "wasm")] { - // Call the function load file with `&mut self` and await it on the main thread - let shared_self = Arc::new(Mutex::new(self.clone_for_load_file())); - let shared_self_clone = Arc::clone(&shared_self); - let future = async move { - let mut shared_self = shared_self_clone.lock().unwrap(); - shared_self.load_file().await; - }; - wasm_bindgen_futures::spawn_local(future); - // Wait for the result - let shared_self = shared_self.lock().unwrap(); - self.tm = shared_self.tm.clone(); - self.code = shared_self.code.clone(); - - console_log!("Retrieved code: {}", self.code); + console_log!("Opening..."); - self.error = shared_self.error.clone(); - self.file = shared_self.file.clone(); + self.file_request_future = + Some(poll_promise::Promise::spawn_async(Self::load_file())); } #[cfg(not(target_family = "wasm"))] - self.load_file(); + { + debug!("Opening..."); + self.load_file(); + } } else if i.modifiers.shift && i.consume_shortcut(&egui::KeyboardShortcut::new( egui::Modifiers::CTRL, @@ -1136,14 +1284,30 @@ impl eframe::App for MyApp { )) { // Ctrl+Shift+S - debug!("Saving as..."); - self.save_file_as(); + #[cfg(target_family = "wasm")] + { + console_log!("Opening..."); + + self.file_request_future = + Some(poll_promise::Promise::spawn_async(Self::load_file())); + } + + #[cfg(not(target_family = "wasm"))] + { + debug!("Saving as..."); + self.save_file_as(); + } } else if i.consume_shortcut(&egui::KeyboardShortcut::new( egui::Modifiers::COMMAND, egui::Key::R, )) { // Ctrl+R + #[cfg(not(target_family = "wasm"))] debug!("Restarting..."); + + #[cfg(target_family = "wasm")] + console_log!("Restarting TM..."); + self.tm = self.tm.restart(&self.code).unwrap(); } }); diff --git a/src/windows/compsition_help_window.rs b/src/windows/compsition_help_window.rs index 8a25476..f1dce5f 100644 --- a/src/windows/compsition_help_window.rs +++ b/src/windows/compsition_help_window.rs @@ -1,4 +1,7 @@ -use eframe::egui::{self, RichText}; +use eframe::{ + egui::{self, RichText, TextEdit}, + epaint::Color32, +}; use egui_extras::{Column, TableBuilder}; use turing_lib::LIBRARIES; @@ -28,79 +31,121 @@ impl SecondaryWindow for CompositionHelpWindow { egui::Window::new(t!("title.composition", self.lang)) .id(egui::Id::new("composition_help_window")) - .resizable(false) + .resizable(true) + .default_size([1000.0, 300.0]) .open(&mut active) .show(ctx, |ui| { - TableBuilder::new(ui) + egui::ScrollArea::horizontal() .auto_shrink([true, true]) - .striped(true) - .resizable(true) - .cell_layout(egui::Layout::centered_and_justified( - egui::Direction::LeftToRight, - )) - .columns(Column::auto(), 4) - .column(Column::auto().clip(true).resizable(true)) - .header(20.0, |mut header| { - header.col(|ui| { - ui.label( - RichText::new(t!("lbl.composition.name", self.lang)).heading(), - ) - .on_hover_text_at_pointer(t!("tooltip.composition.name", self.lang)); - }); - - header.col(|ui| { - ui.label( - RichText::new(t!("lbl.composition.description", self.lang)) - .heading(), - ); - }); - - header.col(|ui| { - ui.label(RichText::new(t!("lbl.state.initial", self.lang)).heading()); - }); - - header.col(|ui| { - ui.label(RichText::new(t!("lbl.state.final", self.lang)).heading()); - }); - - header.col(|ui| { - ui.label(RichText::new(t!("lbl.state.used", self.lang)).heading()); - }); - }) - .body(|mut body| { - for lib in &LIBRARIES { - body.row(20.0, |mut row| { - row.col(|ui| { - ui.label(lib.name.clone()); + .show(ui, |ui| { + TableBuilder::new(ui) + .auto_shrink([true, true]) + .striped(true) + .cell_layout(egui::Layout::centered_and_justified( + egui::Direction::LeftToRight, + )) + .columns(Column::auto(), 4) + .column(Column::auto().clip(true)) + .header(20.0, |mut header| { + header.col(|ui| { + ui.label( + RichText::new(t!("lbl.composition.name", self.lang)) + .heading(), + ) + .on_hover_text_at_pointer(t!( + "tooltip.composition.name", + self.lang + )); }); - row.col(|ui| { - ui.label(lib.description.clone()); + header.col(|ui| { + ui.label( + RichText::new(t!("lbl.composition.description", self.lang)) + .heading(), + ); }); - row.col(|ui| { - ui.label(lib.initial_state.clone()); + header.col(|ui| { + ui.label( + RichText::new(t!("lbl.state.initial", self.lang)).heading(), + ); }); - row.col(|ui| { - ui.label(lib.final_state.clone()); + header.col(|ui| { + ui.label( + RichText::new(t!("lbl.state.final", self.lang)).heading(), + ); }); - row.col(|ui| { - egui::ScrollArea::horizontal() - .auto_shrink([true, true]) - .id_source(String::from(lib.name.clone()) + "_scroll") - .show(ui, |ui| { - ui.label(lib.used_states.join(", ").clone()); - }); + header.col(|ui| { + ui.label( + RichText::new(t!("lbl.state.used", self.lang)).heading(), + ); }); + }) + .body(|mut body| { + for lib in &LIBRARIES { + body.row(30.0, |mut row| { + row.col(|ui| { + ui.label(lib.name.clone()); + }); + + row.col(|ui| { + ui.label(lib.description.clone()); + }); + + row.col(|ui| { + ui.label(lib.initial_state.clone()); + }); + + row.col(|ui| { + ui.label(lib.final_state.clone()); + }); + + row.col(|ui| { + ui.horizontal(|ui| { + ui.label(lib.used_states.join(", ").clone()); + }); + }); + }); + } }); - } - }); - ui.separator(); + ui.separator(); + + ui.label(RichText::new(t!("lbl.composition.help.txt", self.lang))) + .on_hover_ui_at_pointer(|ui| { + ui.vertical_centered(|ui| { + let mut sample_code = String::from( + " + /// a + b + 1 + + {11111011}; + + I = {q0}; F = {q3}; + + compose = {sum}; - ui.label(RichText::new(t!("lbl.composition.help.txt", self.lang))); + (q2, 1, 1, R, q3); + (q3, 1, 1, R, q3); + (q3, 0, 1, H, q3);", + ); + + ui.add_enabled( + false, + TextEdit::multiline(&mut sample_code).code_editor(), + ); + + ui.separator(); + + ui.label( + RichText::new(t!("lbl.composition.warning", self.lang)) + .italics() + .color(Color32::LIGHT_YELLOW), + ); + }); + }) + }); }); active diff --git a/src/windows/workbook/mod.rs b/src/windows/workbook/mod.rs index edf9221..a390ece 100644 --- a/src/windows/workbook/mod.rs +++ b/src/windows/workbook/mod.rs @@ -2,8 +2,6 @@ mod book; mod exercise; mod wb_editor; -use std::io::Read; - pub use book::BookWindow as WorkbookWindow; pub use wb_editor::WorkbookEditorWindow; @@ -19,15 +17,10 @@ type Workbook = Vec; use { log::{debug, error}, rfd, + std::io::Read, std::{fs::File, io::Write, path::PathBuf}, }; -#[cfg(target_arch = "wasm32")] -use { - base64::engine::general_purpose::STANDARD_NO_PAD as base64, js_sys, wasm_bindgen::prelude::*, - wasm_bindgen::JsCast, web_sys, -}; - const MAX_IMG_SIZE: egui::Vec2 = egui::Vec2::new(600.0, 250.0); #[cfg(not(target_arch = "wasm32"))]