diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 7baaf4c..8ad1dbb 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -2,11 +2,11 @@ name: Rust on: push: - branches: [main] + branches: [main, dev_master] tags: - "*" pull_request: - branches: [main] + branches: [main, dev_master] env: CARGO_TERM_COLOR: always diff --git a/CHANGELOG.md b/CHANGELOG.md index 4053b42..24aa2c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [v1.2.2] +### Added +- Help for the composition window +- The missing translations for the application + +### Changed +- Made the layout responsive + ## [v1.2.1] ### Added - Composition support (#23) diff --git a/Cargo.lock b/Cargo.lock index d86f606..a2429f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -284,7 +284,7 @@ dependencies = [ "async-lock", "async-task", "concurrent-queue", - "fastrand", + "fastrand 1.9.0", "futures-lite", "slab", ] @@ -356,7 +356,7 @@ checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.27", ] [[package]] @@ -367,13 +367,13 @@ checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae" [[package]] name = "async-trait" -version = "0.1.71" +version = "0.1.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf" +checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.27", ] [[package]] @@ -534,7 +534,7 @@ dependencies = [ "async-lock", "async-task", "atomic-waker", - "fastrand", + "fastrand 1.9.0", "futures-lite", "log", ] @@ -562,7 +562,7 @@ checksum = "fdde5c9cd29ebd706ce1b35600920a33550e402fc998a2e53ad3b42c3c47a192" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.27", ] [[package]] @@ -649,9 +649,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.3.12" +version = "4.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eab9e8ceb9afdade1ab3f0fd8dbce5b1b2f468ad653baf10e771781b2b67b73" +checksum = "5fd304a20bff958a57f04c4e96a2e7594cc4490a0e809cbd48bb6437edaa452d" dependencies = [ "clap_builder", "clap_derive", @@ -670,9 +670,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.3.12" +version = "4.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f2763db829349bf00cfc06251268865ed4363b93a943174f638daf3ecdba2cd" +checksum = "01c6a3f08f1fe5662a35cfe393aec09c4df95f60ee93b7556505260f75eee9e1" dependencies = [ "anstream", "anstyle", @@ -689,7 +689,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.27", ] [[package]] @@ -1047,7 +1047,7 @@ checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.27", ] [[package]] @@ -1132,6 +1132,12 @@ dependencies = [ "instant", ] +[[package]] +name = "fastrand" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" + [[package]] name = "fdeflate" version = "0.3.0" @@ -1178,7 +1184,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.27", ] [[package]] @@ -1256,7 +1262,7 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" dependencies = [ - "fastrand", + "fastrand 1.9.0", "futures-core", "futures-io", "memchr", @@ -1273,7 +1279,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.27", ] [[package]] @@ -2082,9 +2088,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg", ] @@ -2128,7 +2134,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.27", ] [[package]] @@ -2300,9 +2306,9 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pest" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f73935e4d55e2abf7f130186537b19e7a4abc886a0252380b59248af473a3fc9" +checksum = "0d2d1d55045829d65aad9d389139882ad623b33b904e7c9f1b10c5b8927298e5" dependencies = [ "thiserror", "ucd-trie", @@ -2310,9 +2316,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef623c9bbfa0eedf5a0efba11a5ee83209c326653ca31ff019bec3a95bfff2b" +checksum = "5f94bca7e7a599d89dea5dfa309e217e7906c3c007fb9c3299c40b10d6a315d3" dependencies = [ "pest", "pest_generator", @@ -2320,22 +2326,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e8cba4ec22bada7fc55ffe51e2deb6a0e0db2d0b7ab0b103acc80d2510c190" +checksum = "99d490fe7e8556575ff6911e45567ab95e71617f43781e5c05490dc8d75c965c" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.27", ] [[package]] name = "pest_meta" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a01f71cb40bd8bb94232df14b946909e14660e33fc05db3e50ae2a82d7ea0ca0" +checksum = "2674c66ebb4b4d9036012091b537aae5878970d6999f81a265034d85b136b341" dependencies = [ "once_cell", "pest", @@ -2407,9 +2413,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.65" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92de25114670a878b1261c79c9f8f729fb97e95bac93f6312f583c60dd6a1dfe" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" dependencies = [ "unicode-ident", ] @@ -2422,9 +2428,9 @@ checksum = "332cd62e95873ea4f41f3dfd6bbbfc5b52aec892d7e8d534197c4720a0bbbab2" [[package]] name = "quote" -version = "1.0.30" +version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5907a1b7c277254a8b15170f6e7c97cfa60ee7872a3217663bb81151e48184bb" +checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" dependencies = [ "proc-macro2", ] @@ -2602,9 +2608,9 @@ checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" [[package]] name = "scopeguard" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sctk-adwaita" @@ -2621,9 +2627,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.171" +version = "1.0.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" +checksum = "5d25439cd7397d044e2748a6fe2432b5e85db703d6d097bd014b3c0ad1ebff0b" dependencies = [ "serde_derive", ] @@ -2639,13 +2645,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.171" +version = "1.0.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682" +checksum = "b23f7ade6f110613c0d63858ddb8b94c1041f550eab58a16b371bdf2c9c80ab4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.27", ] [[package]] @@ -2661,13 +2667,13 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d89a8107374290037607734c0b73a85db7ed80cae314b3c5791f192a496e731" +checksum = "e168eaaf71e8f9bd6037feb05190485708e019f4fd87d161b3c0a0d37daf85e5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.27", ] [[package]] @@ -2712,9 +2718,9 @@ dependencies = [ [[package]] name = "signal-hook" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b824b6e687aff278cdbf3b36f07aa52d4bd4099699324d5da86a2ebce3aa00b3" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" dependencies = [ "libc", "signal-hook-registry", @@ -2731,9 +2737,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "slab" @@ -2845,15 +2851,25 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.26" +version = "2.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45c3457aacde3c65315de5031ec191ce46604304d2446e803d71ade03308d970" +checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "sys-locale" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0b9eefabb91675082b41eb94c3ecd91af7656caee3fb4961a07c0ec8c7ca6f" +dependencies = [ + "libc", + "windows-sys 0.45.0", +] + [[package]] name = "system-deps" version = "6.1.1" @@ -2869,21 +2885,20 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.9" +version = "0.12.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8e77cb757a61f51b947ec4a7e3646efd825b73561db1c232a8ccb639e611a0" +checksum = "1d2faeef5759ab89935255b1a4cd98e0baf99d1085e37d36599c625dac49ae8e" [[package]] name = "tempfile" -version = "3.6.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" +checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998" dependencies = [ - "autocfg", "cfg-if", - "fastrand", + "fastrand 2.0.0", "redox_syscall", - "rustix 0.37.23", + "rustix 0.38.4", "windows-sys 0.48.0", ] @@ -2898,22 +2913,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42" +checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f" +checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.27", ] [[package]] @@ -3020,7 +3035,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.27", ] [[package]] @@ -3077,9 +3092,9 @@ checksum = "a464a4b34948a5f67fddd2b823c62d9d92e44be75058b99939eae6c5b6960b33" [[package]] name = "turing-lib" -version = "2.1.1" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74a43889a0e6c88576f9bad20f06102fa9814abc17961e7de42df9deb8fa3946" +checksum = "a299abbb88bfe08446427f19f116a56c29a298460c59a313e951471017c30d2c" dependencies = [ "env_logger", "log", @@ -3090,7 +3105,7 @@ dependencies = [ [[package]] name = "turing-machine" -version = "1.2.1" +version = "1.2.2" dependencies = [ "base64", "bincode", @@ -3110,6 +3125,7 @@ dependencies = [ "rfd", "serde", "serde_bytes", + "sys-locale", "tracing-subscriber", "tracing-wasm", "turing-lib", @@ -3259,7 +3275,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.27", "wasm-bindgen-shared", ] @@ -3293,7 +3309,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.27", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3763,9 +3779,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fac9742fd1ad1bd9643b991319f72dd031016d44b77039a26977eb667141e7" +checksum = "25b5872fa2e10bd067ae946f927e726d7d603eaeb6e02fa6a350e0722d2b8c11" dependencies = [ "memchr", ] @@ -3824,9 +3840,9 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.15" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a56c84a8ccd4258aed21c92f70c0f6dea75356b6892ae27c24139da456f9336" +checksum = "47430998a7b5d499ccee752b41567bc3afc57e1327dc855b1a2aa44ce29b5fa1" [[package]] name = "zbus" diff --git a/Cargo.toml b/Cargo.toml index 42bdf99..3f4a61e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "turing-machine" -version = "1.2.1" +version = "1.2.2" edition = "2021" authors = ["Marcos Gutiérrez Alonso "] description = "Turing Machine Simulator" @@ -25,6 +25,7 @@ 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] tracing-subscriber = "0.3" @@ -43,8 +44,10 @@ js-sys = "^0.3" base64 = "^0.21" [profile.release] -opt-level = 2 # fast and small wasm - +panic = 'abort' # Do not perform backtrace for panic on release builds. +opt-level = 'z' # Optimize for size +codegen-units = 1 # Perform optimizations on all codegen units. +lto = true # Perform link-time optimizations (slower build time) [features] # no features by default diff --git a/index.html b/index.html index c266752..f5f0496 100644 --- a/index.html +++ b/index.html @@ -10,7 +10,7 @@ Turing Machine - + diff --git a/locales/errors.json b/locales/errors.json new file mode 100644 index 0000000..5d380e0 --- /dev/null +++ b/locales/errors.json @@ -0,0 +1,22 @@ +{ + "err.initialization": { + "en": "Could not initialize the Turing Machine. Please fix the syntax error and try again.", + "es": "No se pudo inicializar la Máquina de Turing. Por favor, corrija el error de sintaxis e inténtelo de nuevo." + }, + "err.undefined.state": { + "en": "The machine is in an undefined state", + "es": "La máquina está en un estado indefinido" + }, + "err.infinite_loop": { + "en": "Infinite loop", + "es": "Bucle infinito" + }, + "err.syntax.simple": { + "en": "Syntax error", + "es": "Error de sintaxis" + }, + "err.syntax": { + "en": "Syntax error on file $file", + "es": "Error de sintaxis en el archivo $file" + } +} \ No newline at end of file diff --git a/locales/main_window.json b/locales/main_window.json index 40d6aa1..fff603c 100644 --- a/locales/main_window.json +++ b/locales/main_window.json @@ -1,4 +1,8 @@ { + "menu.file": { + "en": "File", + "es": "Archivo" + }, "menu.about": { "en": "About", "es": "Acerca de" @@ -19,6 +23,14 @@ "en": "Activate debugger", "es": "Activar depurador" }, + "menu.exercises": { + "en": "Exercises", + "es": "Ejercicios" + }, + "menu.exercises.editor": { + "en": "Workbook editor", + "es": "Editor de cuadernos" + }, "btn.open_file": { "en": "Open File", "es": "Abrir Archivo" @@ -31,6 +43,10 @@ "en": "Compile and run code", "es": "Compilar y ejecutar código" }, + "btn.libraries": { + "en": "Show available libraries for composition", + "es": "Mostrar librerías disponibles para composición" + }, "lbl.tape.size": { "en": "Tape rectangle size", "es": "Tamaño de la cinta" @@ -44,8 +60,8 @@ "es": "Umbral de bucle infinito" }, "lbl.machine.step": { - "en": "Do one step", - "es": "Dar un paso" + "en": "Run a single instruction", + "es": "Ejecutar una instrucción" }, "lbl.seconds": { "en": " seconds", @@ -68,16 +84,16 @@ "es": "Pausar" }, "lbl.paused": { - "en":"The application is paused.\nTo unpause it, press the spacebar or this button:", - "es":"La aplicación está pausada.\nPara reanudarla, presione la barra espaciadora o este botón:" + "en":"The application is paused.", + "es":"La aplicación está pausada." }, "lbl.resume": { "en": "Resume", "es": "Reanudar" }, "lbl.resumed": { - "en": "The simulation is running.\nTo pause it, press the spacebar or this button:", - "es": "La simulación está activa.\nPara pausarla, presione la barra espaciadora o este botón:" + "en": "The simulation is running.", + "es": "La simulación está activa.:" }, "lbl.restart": { "en": "Restart", @@ -87,6 +103,26 @@ "en": "The program has finished.\nTo restart it, press this button:", "es": "El programa ha terminado.\nPara reiniciarlo, presione este botón:" }, + "lbl.state.initial": { + "en": "Initial state", + "es": "Estado inicial" + }, + "lbl.state.final": { + "en": "Final state", + "es": "Estado final" + }, + "lbl.state.used": { + "en": "Used states", + "es": "Estados usados" + }, + "header.sliders": { + "en": "Sliders", + "es": "Controles" + }, + "header.code": { + "en": "Code panel", + "es": "Panel de código" + }, "lang.en": { "en": "English", "es": "Inglés" @@ -95,9 +131,9 @@ "en": "Spanish", "es": "Español" }, - "err.undefined.state": { - "en": "The machine is in an undefined state", - "es": "La máquina está en un estado indefinido" + "btn.close": { + "en": "Close", + "es": "Cerrar" } } \ No newline at end of file diff --git a/locales/tooltips.json b/locales/tooltips.json new file mode 100644 index 0000000..ef9156b --- /dev/null +++ b/locales/tooltips.json @@ -0,0 +1,30 @@ +{ + "tooltip.main.step": { + "en": "Execute one single step of the Turing machine", + "es": "Ejecuta un solo paso de la máquina de Turing" + }, + "tooltip.tape.size": { + "en": "The size of the squares of the drawing of the tape.", + "es": "El tamaño de los cuadrados del dibujo de la cinta." + }, + "tooltip.tape.duration": { + "en": "The duration of the animation of the tape. When a step is executed, the tape will move to the next position in this amount of time.", + "es": "La duración de la animación de la cinta. Cuando se ejecuta un paso, la cinta se moverá a la siguiente posición en esta cantidad de tiempo." + }, + "tooltip.tape.iterations": { + "en": "The maximum number of iterations that the Turing machine can execute before assuming that it is an infinite loop.", + "es": "El número máximo de iteraciones que la máquina de Turing puede ejecutar antes de asumir que es un bucle infinito." + }, + "tooltip.button.playpause": { + "en": "Play/pause the execution of the machine. If the execution has finished, pressing \"play\" will reset the machine.\nThe shortcut is the spacebar.", + "es": "Reproduce/pausa la ejecución de la máquina. Si la ejecución ha terminado, pulsar \"play\" reiniciará la máquina.\nEl atajo es la barra espaciadora." + }, + "tooltip.composition.name": { + "en": "The name to write to import it", + "es": "El nombre a escribir para importarla" + }, + "tooltip.editor.chapter_title": { + "en": "Chapter title", + "es": "Título del capítulo" + } +} \ No newline at end of file diff --git a/locales/about_window.json b/locales/windows/about_window.json similarity index 100% rename from locales/about_window.json rename to locales/windows/about_window.json diff --git a/locales/windows/book_window.json b/locales/windows/book_window.json new file mode 100644 index 0000000..4eb9545 --- /dev/null +++ b/locales/windows/book_window.json @@ -0,0 +1,58 @@ +{ + "title.workbook": { + "en": "Workbook", + "es": "Cuaderno" + }, + "title.workbook.editor": { + "en": "Workbook Editor", + "es": "Editor de cuadernos" + }, + "heading.workbook.catalog": { + "en": "Workbook Catalog", + "es": "Catálogo de Cuadernos" + }, + "btn.workbook.load": { + "en": "Load workbook", + "es": "Cargar cuaderno" + }, + "btn.workbook.previous": { + "en": "Previous", + "es": "Anterior" + }, + "btn.workbook.next": { + "en": "Next", + "es": "Siguiente" + }, + "btn.workbook.use": { + "en": "Use this exercise", + "es": "Usar este ejercicio" + }, + "btn.editor.add_exercise": { + "en": "Add exercise", + "es": "Añadir ejercicio" + }, + "btn.editor.add_chapter": { + "en": "Add chapter", + "es": "Añadir capítulo" + }, + "btn.editor.add_image": { + "en": "Add image", + "es": "Añadir imagen" + }, + "btn.editor.save_workbook": { + "en": "Save workbook", + "es": "Guardar cuaderno" + }, + "lbl.editor.exercise_code": { + "en": "Exercise code", + "es": "Código del ejercicio" + }, + "lbl.editor.new_chapter": { + "en": "New chapter ($num)", + "es": "Nuevo capítulo ($num)" + }, + "lbl.editor.new_exercise": { + "en": "New exercise ($num)", + "es": "Nuevo ejercicio ($num)" + } +} \ No newline at end of file diff --git a/locales/windows/composition_window.json b/locales/windows/composition_window.json new file mode 100644 index 0000000..e958faf --- /dev/null +++ b/locales/windows/composition_window.json @@ -0,0 +1,22 @@ +{ + "title.composition": { + "en": "List of composition libraries available", + "es": "Lista de librerías disponibles para componer" + }, + "lbl.composition.name": { + "en": "Library name", + "es": "Nombre de la librería" + }, + "lbl.composition.description": { + "en": "Description", + "es": "Descripción" + }, + "lbl.composition.help": { + "en": "Help (what is this?)", + "es": "Ayuda (¿qué es esto?)" + }, + "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" + } +} \ No newline at end of file diff --git a/locales/debug_window.json b/locales/windows/debug_window.json similarity index 100% rename from locales/debug_window.json rename to locales/windows/debug_window.json diff --git a/locales/infinite_loop_window.json b/locales/windows/infinite_loop_window.json similarity index 100% rename from locales/infinite_loop_window.json rename to locales/windows/infinite_loop_window.json diff --git a/src/lib.rs b/src/lib.rs index 525ec5d..b3ef96e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,20 @@ mod window; pub mod windows; pub use turing_widget::TuringWidget; -pub use window::MyApp; +pub use window::{Language, MyApp}; + +pub fn get_lang() -> Language { + match sys_locale::get_locale() { + Some(locale) => { + if String::from(&locale[..2]) == String::from("es") { + Language::Spanish + } else { + Language::English + } + } + None => Language::English, + } +} pub struct CompositionLibrary { pub name: String, diff --git a/src/main.rs b/src/main.rs index e15215f..4c40994 100644 --- a/src/main.rs +++ b/src/main.rs @@ -113,6 +113,7 @@ fn load_icon(path: &str) -> Option { #[cfg(not(target_arch = "wasm32"))] fn run_machine_gui(file: Option) { use eframe::egui; + use turing_machine::get_lang; let options = eframe::NativeOptions { drag_and_drop_support: true, @@ -134,7 +135,7 @@ fn run_machine_gui(file: Option) { options, Box::new(move |cc| match MyApp::new(&file, cc) { Ok(w) => Box::new(w), - Err(e) => Box::new(ErrorWindow::new(e, file, cc)), + Err(e) => Box::new(ErrorWindow::new(e, file, get_lang(), cc)), }), ) { Ok(_) => (), diff --git a/src/turing_widget.rs b/src/turing_widget.rs index ac66d90..241b5c4 100644 --- a/src/turing_widget.rs +++ b/src/turing_widget.rs @@ -6,7 +6,10 @@ use internationalization::t; use log::warn; use turing_lib::{CompilerError, CompilerWarning, Library, TuringMachine, TuringOutput}; +use crate::window::is_mobile; + const STROKE_WIDTH: f32 = 3f32; +const FONT_SIZE: f32 = 30f32; #[derive(Debug, Clone)] /// A widget that displays a Turing machine @@ -101,7 +104,7 @@ impl TuringWidget { self.tm.step(); self.offset = self.tm.tape_position as f32 - prev as f32; - if self.tm.finished() + if self.finished() || self.tm.is_undefined() || self.tm.is_infinite_loop(self.threshold_inf_loop) { @@ -111,6 +114,21 @@ impl TuringWidget { return self.offset; } + /// Returns whether the turing machine is in a final state, the current state is the same as the previous state and the current instruction is HALT + pub fn finished(&self) -> bool { + self.tm.finished() + && self.tm.previous_state.clone().unwrap_or_default() == self.tm.current_state + && match self.tm.get_current_instruction() { + Some(ins) => { + println!("{:?}", ins); + ins.movement == turing_lib::Movement::HALT + && ins.from_value == ins.to_value + && ins.from_state == ins.to_state + } + None => true, + } + } + /// Returns the current tape value pub fn tape_value(&self) -> TuringOutput { self.tm.tape_value() @@ -159,11 +177,6 @@ impl TuringWidget { .collect::>() } - /// Returns whether the Turing machine has finished - pub fn finished(&self) -> bool { - self.tm.finished() - } - /// Reset the frequencies of the Turing machine pub fn reset_frequencies(&mut self) { self.tm.reset_frequencies(); @@ -178,12 +191,29 @@ impl TuringWidget { impl Widget for &mut TuringWidget { /// Paints the Turing machine fn ui(self, ui: &mut eframe::egui::Ui) -> eframe::egui::Response { + let mut font_id = self.font_id.clone(); + let mut stroke_width = self.stroke_width; + let mut tri_size = self.tri_size; + + if is_mobile(ui.ctx()) { + font_id.size = FONT_SIZE / 2.0; + stroke_width /= 2.0; + tri_size /= 1.5; + } + if ui.is_rect_visible(ui.cursor()) { - let stroke = Stroke::new(self.stroke_width, Color32::BLACK); + let stroke = Stroke::new(stroke_width, Color32::BLACK); let rounding = Rounding::same(10f32); let size = Vec2::new(self.tape_rect_size, self.tape_rect_size); - let center = - ui.cursor().center_top() + Vec2::new(0.0, self.tape_rect_size / 2.0 + 50.0); + let center = ui.cursor().center_top() + + Vec2::new( + if is_mobile(ui.ctx()) { + -ui.available_width() / 2.0 + ui.ctx().screen_rect().width() / 2.0 + } else { + 0.0 + }, + self.tape_rect_size / 2.0 + if is_mobile(ui.ctx()) { 25.0 } else { 50.0 }, + ); let pos = center + Vec2::new((self.offset as f32) * size.x, 0.0); @@ -213,20 +243,22 @@ impl Widget for &mut TuringWidget { } } + let height = if is_mobile(ui.ctx()) { 1.2 } else { 1.0 }; + let c1: Pos2 = center + Vec2::new( - self.tri_size / 1.75 - self.tri_stroke_wid * 2.0, - self.tri_size, + height * tri_size / 1.75 - self.tri_stroke_wid * 2.0, + height * tri_size, ); let c2: Pos2 = center + Vec2::new( - -self.tri_size / 1.75 + self.tri_stroke_wid * 2.0, - self.tri_size, + -height * tri_size / 1.75 + self.tri_stroke_wid * 2.0, + height * tri_size, ); let c3: Pos2 = center + Vec2::new(0.0, self.tape_rect_size / 3.0); - let circle_rad = self.tri_size / 2.0; - let circle_center = center + Vec2::new(0.0, self.tri_size + 25.0); + let circle_rad = tri_size / 2.0 + if is_mobile(ui.ctx()) { 0.0 } else { 0.5 }; + let circle_center = center + Vec2::new(0.0, tri_size + 25.0); ui.painter().line_segment([c2, c3], self.tri_stroke); ui.painter().line_segment([c3, c1], self.tri_stroke); @@ -259,7 +291,7 @@ impl Widget for &mut TuringWidget { center + Vec2::new(0.0, self.tri_size + 100.0), Align2::CENTER_CENTER, t!("err.undefined.state", self.lang), - self.font_id.clone(), + font_id.clone(), Color32::LIGHT_RED, ); self.paused = true; @@ -267,8 +299,8 @@ impl Widget for &mut TuringWidget { ui.painter().text( center + Vec2::new(0.0, self.tri_size + 100.0), Align2::CENTER_CENTER, - "Infinite loop", //t!("err.infinite.loop", self.lang), // TODO: Translation - self.font_id.clone(), + t!("err.infinite_loop", self.lang), + font_id.clone(), Color32::LIGHT_RED, ); self.paused = true; @@ -281,10 +313,13 @@ impl Widget for &mut TuringWidget { center + Vec2::new(0.0, self.tri_size + 150.0), Align2::CENTER_CENTER, "The machine is in a final state", - self.font_id.clone(), + font_id, Color32::LIGHT_GREEN, ); - self.paused = true; + + if self.finished() { + self.paused = true; + } } } diff --git a/src/window.rs b/src/window.rs index cf33ff9..d185549 100644 --- a/src/window.rs +++ b/src/window.rs @@ -6,6 +6,7 @@ use std::{ }; use crate::{ + get_lang, windows::{ AboutWindow, CompositionHelpWindow, DebugWindow, InfiniteLoopWindow, SecondaryWindow, WorkbookEditorWindow, WorkbookWindow, @@ -28,6 +29,11 @@ use { }; const DEFAULT_CODE: &str = include_str!("../Examples/Example1.tm"); +const MOBILE_THRESHOLD: f32 = 500.0; + +pub fn is_mobile(ctx: &egui::Context) -> bool { + ctx.screen_rect().width() < MOBILE_THRESHOLD +} // Import the saveFile function //#[cfg(target_arch = "wasm32")] @@ -111,7 +117,7 @@ impl MyApp { workbook_editor_window: None, composition_help_window: None, - lang: Language::English, + lang: get_lang(), file: file.clone(), autosave: file.is_some(), @@ -215,7 +221,7 @@ impl MyApp { ui.add_enabled_ui(!editor_focused, |ui| { if self.tm.offset != 0.0 { ui.add_enabled(false, |ui: &mut Ui| ui.button(t!("lbl.machine.step", lang))) - .on_hover_text_at_pointer("Execute one single step of the Turing machine"); // TODO: Translate + .on_hover_text_at_pointer(t!("tooltip.main.step", lang)); if self.tm.offset.abs() < 0.01 { self.tm.offset = 0.0; @@ -231,8 +237,7 @@ impl MyApp { } else if (ui .add_enabled(self.tm.paused, |ui: &mut Ui| { ui.button(t!("lbl.machine.step", lang)) - .on_hover_text_at_pointer("Execute one single step of the Turing machine") - // TODO: Translate + .on_hover_text_at_pointer(t!("tooltip.main.step", lang)) }) .clicked() || ui.input(|i| i.key_pressed(egui::Key::ArrowRight)) @@ -643,109 +648,132 @@ impl MyApp { /// * lang - A string representing the language used for displaying text in the UI fn draw_top_panel(&mut self, ctx: &egui::Context, lang: &str) { egui::TopBottomPanel::top("header") - .default_height(35.0) + .default_height(if is_mobile(ctx) { 50.0 } else { 35.0 }) .show(ctx, |ui| { - ui.horizontal_centered(|ui| { - ui.menu_button("File", |ui| { - if ui - .add(egui::Button::new("Open").shortcut_text("Ctrl + O")) - .clicked() - { - #[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(); - } - - #[cfg(not(target_family = "wasm"))] - self.load_file(); - } - - if ui - .add(egui::Button::new("Save").shortcut_text("Ctrl + S")) - .clicked() - { - self.save_file(); - } + egui::ScrollArea::horizontal() + .max_width(ctx.screen_rect().width()) + .show(ui, |ui| { + ui.horizontal_centered(|ui| { + ui.menu_button(t!("menu.file", lang), |ui| { + if ui + .add(egui::Button::new("Open").shortcut_text("Ctrl + O")) + .clicked() + { + #[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(); + } + + #[cfg(not(target_family = "wasm"))] + self.load_file(); + } - if ui - .add(egui::Button::new("Save as...").shortcut_text("Ctrl + Shift + S")) - .clicked() - { - self.save_file_as(); - } + if ui + .add(egui::Button::new("Save").shortcut_text("Ctrl + S")) + .clicked() + { + self.save_file(); + } - ui.add_enabled_ui(self.file.is_some(), |ui| { - ui.checkbox(&mut self.autosave, "Autosave") - }); - }); + if ui + .add( + egui::Button::new("Save as...") + .shortcut_text("Ctrl + Shift + S"), + ) + .clicked() + { + self.save_file_as(); + } - if ui.button(t!("menu.debugger", lang)).clicked() { - if self.debug_window.is_none() { - self.debug_window = Some(Box::new(DebugWindow::new( - &lang, - Some(self.tm.tape_values()), - Some(self.tm.tape_value()), - Some(egui::Pos2::new(0.0, 100.0)), - ))); - } - } + ui.add_enabled_ui(self.file.is_some(), |ui| { + ui.checkbox(&mut self.autosave, "Autosave") + }); + }); - if cfg!(feature = "teacher") { - ui.menu_button("Exercises", |ui| { - if ui.button("Exercises").clicked() && self.book_window.is_none() { - self.book_window = - Some(Box::new(WorkbookWindow::new(&self.get_lang()))); + if ui.button(t!("menu.debugger", lang)).clicked() { + if self.debug_window.is_none() { + self.debug_window = Some(Box::new(DebugWindow::new( + &lang, + Some(self.tm.tape_values()), + Some(self.tm.tape_value()), + Some(egui::Pos2::new(0.0, 100.0)), + ))); + } } - if ui.button("Workbook editor").clicked() - && self.workbook_editor_window.is_none() - { - self.workbook_editor_window = - Some(Box::new(WorkbookEditorWindow::new(&self.get_lang()))); + if cfg!(feature = "teacher") { + ui.menu_button(t!("menu.exercises", lang), |ui| { + if ui.button(t!("menu.exercises", lang)).clicked() + && self.book_window.is_none() + { + self.book_window = + Some(Box::new(WorkbookWindow::new(&self.get_lang()))); + } + + if ui.button(t!("menu.exercises.editor", lang)).clicked() + && self.workbook_editor_window.is_none() + { + self.workbook_editor_window = Some(Box::new( + WorkbookEditorWindow::new(&self.get_lang()), + )); + } + }); + } else { + if ui.button(t!("menu.exercises", lang)).clicked() + && self.book_window.is_none() + { + self.book_window = + Some(Box::new(WorkbookWindow::new(&self.get_lang()))); + } } - }); - } else { - if ui.button("Exercises").clicked() && self.book_window.is_none() { - self.book_window = - Some(Box::new(WorkbookWindow::new(&self.get_lang()))); - } - } - 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)); - }); + 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), + ); + }); - ui.menu_button(t!("menu.about", lang), |ui| { - if ui.button(t!("menu.about", lang)).clicked() { - self.about_window = Some(Box::new(AboutWindow::new( - &lang, - Some(egui::Pos2::new(150.0, 100.0)), - ))); - } + ui.menu_button(t!("menu.about", lang), |ui| { + if ui.button(t!("menu.about", lang)).clicked() { + self.about_window = Some(Box::new(AboutWindow::new( + &lang, + Some(egui::Pos2::new(150.0, 100.0)), + ))); + } - if ui.link(t!("menu.repository", lang)).clicked() { - webbrowser::open("https://github.com/margual56/turing-machine-2.0") - .unwrap(); - } + if ui.link(t!("menu.repository", lang)).clicked() { + webbrowser::open( + "https://github.com/margual56/turing-machine-2.0", + ) + .unwrap(); + } + }); + }); }); - }); }); } @@ -767,154 +795,159 @@ impl MyApp { lang: &str, editor_focused: &mut bool, ) -> f32 { - egui::SidePanel::left("left") - .show(ctx, |ui| { - ui.vertical_centered_justified(|ui| { - ui.add_space(10.0); + let contents = |ui: &mut egui::Ui| { + ui.vertical_centered_justified(|ui| { + ui.add_space(10.0); - ui.horizontal(|ui| { - let spacer = 10.0; + ui.horizontal(|ui| { + let spacer = 10.0; - if ui - .add(egui::Button::new(t!("btn.open_file", lang)).min_size( - ui.available_size() / 2.0 - egui::Vec2::new(spacer / 2.0, 0.0), - )) - .clicked() + if ui + .add(egui::Button::new(t!("btn.open_file", lang)).min_size( + ui.available_size() / 2.0 - egui::Vec2::new(spacer / 2.0, 0.0), + )) + .clicked() + { + #[cfg(target_family = "wasm")] { - #[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(); - } - - #[cfg(not(target_family = "wasm"))] - self.load_file(); + // 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(); } - ui.add_space(spacer); + #[cfg(not(target_family = "wasm"))] + self.load_file(); + } - if ui - .add( - egui::Button::new(t!("btn.save_file", lang)) - .min_size(ui.available_size()), - ) - .clicked() - { - self.save_file(); - } - }); + ui.add_space(spacer); if ui - .button(egui::RichText::new(t!("btn.compile", lang)).strong()) + .add( + egui::Button::new(t!("btn.save_file", lang)) + .min_size(ui.available_size()), + ) .clicked() { - self.tm = match self.tm.restart(&self.code) { - Ok(t) => { - self.error = None; - t - } - Err(e) => { - self.error = Some(e); - self.tm.clone() - } - }; + self.save_file(); } + }); - if self.tm.uses_libraries() { - ui.separator(); - - egui::ScrollArea::vertical() - .id_source("Library help scroll area") - .max_height(ui.available_height() / 2.0) - .show(ui, |ui| { - for lib in self.tm.libraries() { - ui.collapsing(String::from(lib.name.clone()), |ui| { - egui::ScrollArea::horizontal().show(ui, |ui| { - ui.horizontal(|ui| { - ui.label("Initial state:"); // TODO: Translate - ui.label( - egui::RichText::new(lib.initial_state.clone()) - .strong(), - ); - }); - ui.add_space(5.0); - - ui.horizontal(|ui| { - ui.label("Final state:"); // TODO: Translate - ui.label( - egui::RichText::new(lib.final_state.clone()) - .strong(), - ); - }); - ui.add_space(5.0); - - ui.horizontal(|ui| { - ui.label("Used states:"); // TODO: Translate - ui.label( - egui::RichText::new( - &lib.used_states.join(", "), - ) - .strong(), - ); - }); - }); - }) - .header_response - .on_hover_text_at_pointer(lib.description.clone()); - } - }); - } + if ui + .button(egui::RichText::new(t!("btn.compile", lang)).strong()) + .clicked() + { + self.tm = match self.tm.restart(&self.code) { + Ok(t) => { + self.error = None; + t + } + Err(e) => { + self.error = Some(e); + self.tm.clone() + } + }; + } + + if self.tm.uses_libraries() { + ui.separator(); egui::ScrollArea::vertical() - .max_height(ui.available_height() - 50.0) - .show(ui, |my_ui: &mut Ui| { - let editor = TextEdit::multiline(&mut self.code) - .code_editor() - .desired_width(0.0); + .id_source("Library help scroll area") + .max_height(ui.available_height() / 2.0) + .show(ui, |ui| { + for lib in self.tm.libraries() { + ui.collapsing(String::from(lib.name.clone()), |ui| { + egui::ScrollArea::horizontal().show(ui, |ui| { + ui.horizontal(|ui| { + ui.label(t!("lbl.state.initial", lang) + ":"); + ui.label( + egui::RichText::new(lib.initial_state.clone()) + .strong(), + ); + }); + ui.add_space(5.0); - let res = my_ui.add(editor); + ui.horizontal(|ui| { + ui.label(t!("lbl.state.final", lang) + ":"); + ui.label( + egui::RichText::new(lib.final_state.clone()) + .strong(), + ); + }); + ui.add_space(5.0); - if self.autosave && res.lost_focus() { - debug!("Saving file"); - self.saved_feedback = self.auto_save_file(); + ui.horizontal(|ui| { + ui.label(t!("lbl.state.used", lang) + ":"); + ui.label( + egui::RichText::new(&lib.used_states.join(", ")) + .strong(), + ); + }); + }); + }) + .header_response + .on_hover_text_at_pointer(lib.description.clone()); } - - *editor_focused = res.has_focus().clone(); }); + } - if ui - .button("Show available libraries for composition") - .clicked() - { - // TODO: Translate - self.composition_help_window = - Some(Box::new(CompositionHelpWindow::new(&self.get_lang()))); - } + egui::ScrollArea::vertical() + .max_height(ui.available_height() - 50.0) + .show(ui, |my_ui: &mut Ui| { + let editor = TextEdit::multiline(&mut self.code) + .code_editor() + .desired_width(0.0); - if self.saved_feedback.is_some() { - debug!("Drawing saved feedback popup"); - self.draw_saved_feedback_popup(ui, ctx); - } - }) + let res = my_ui.add(editor); + + if self.autosave && res.lost_focus() { + debug!("Saving file"); + self.saved_feedback = self.auto_save_file(); + } + + *editor_focused = res.has_focus().clone(); + }); + + if ui.button(t!("btn.libraries", lang)).clicked() { + self.composition_help_window = + Some(Box::new(CompositionHelpWindow::new(&self.get_lang()))); + } + + if self.saved_feedback.is_some() { + debug!("Drawing saved feedback popup"); + self.draw_saved_feedback_popup(ui, ctx); + } }) - .response - .rect - .right() + }; + + if is_mobile(ctx) { + egui::Window::new(t!("header.code", lang)) + .collapsible(true) + .default_pos(egui::pos2(0.0, 0.0)) + .constrain(true) + .show(ctx, contents); + return 0.0; + } else { + egui::SidePanel::left("left") + .show(ctx, contents) + .response + .rect + .right() + } } /// Draws the central panel containing the Turing machine description, sliders for tape size, animation speed, @@ -939,21 +972,36 @@ impl MyApp { ); } - ui.add( - egui::Slider::new(&mut self.tm.tape_rect_size, 25.0..=300.0) - .suffix(" px") - .text(t!("lbl.tape.size", lang)) - ).on_hover_text_at_pointer("The size of the squares and text of the drawing of the tape."); // TODO: Translate - ui.add( - egui::Slider::new(&mut self.tm.tape_anim_speed, 0.2..=2.0) - .suffix(t!("lbl.seconds", lang)) - .text(t!("lbl.tape.speed", lang)), - ).on_hover_text_at_pointer("The duration of the animation of the tape. When a step is executed, the tape will move to the next position in this amount of time."); // TODO: Translate - ui.add( - egui::Slider::new(&mut self.tm.threshold_inf_loop, 10..=2000) - .suffix(t!("lbl.iterations", lang)) - .text(t!("lbl.tape.inf_loop", lang)), - ).on_hover_text_at_pointer("The maximum number of iterations that the Turing machine can execute before assuming that it is an infinite loop."); // TODO: Translate + let mut sliders = |ui: &mut egui::Ui| { + ui.add( + egui::Slider::new(&mut self.tm.tape_rect_size, 25.0..=300.0) + .suffix(" px") + .text(t!("lbl.tape.size", lang)), + ) + .on_hover_text_at_pointer(t!("tooltip.tape.size", lang)); + ui.add( + egui::Slider::new(&mut self.tm.tape_anim_speed, 0.2..=2.0) + .suffix(t!("lbl.seconds", lang)) + .text(t!("lbl.tape.speed", lang)), + ) + .on_hover_text_at_pointer(t!("tooltip.tape.duration", lang)); + ui.add( + egui::Slider::new(&mut self.tm.threshold_inf_loop, 10..=2000) + .suffix(t!("lbl.iterations", lang)) + .text(t!("lbl.tape.inf_loop", lang)), + ) + .on_hover_text_at_pointer(t!("tooltip.tape.iterations", lang)); + }; + + if is_mobile(ctx) { + ui.collapsing(t!("header.sliders", lang), |ui| { + egui::ScrollArea::horizontal() + .max_width(ctx.screen_rect().width()) + .show(ui, sliders); + }); + } else { + sliders(ui); + } }); ui.separator(); @@ -984,30 +1032,46 @@ impl MyApp { } else { ui.label(t!("lbl.resumed", lang)); } - let b = ui.button(text).on_hover_text_at_pointer("Play/pause the execution of the machine. If the execution has finished, pressing \"play\" will reset the machine.\nThe shortcut is the spacebar."); // TODO: Translate - if (b.clicked() - || ui.input_mut(|i| { - i.consume_key(egui::Modifiers::NONE, egui::Key::Space) - })) - && !editor_focused - { - if self.tm.finished() { - self.tm = self.tm.restart(&self.code).unwrap(); - } else { - self.tm.paused = !self.tm.paused; - } - } - - if self.process_turing_controls(ui, &ctx, editor_focused, &lang) { - ctx.request_repaint(); - if self.tm.is_inf_loop() { - warn!("Infinite loop detected!"); - self.infinite_loop_window = - Some(Box::new(InfiniteLoopWindow::new(&self.get_lang()))); - self.tm.paused = true; - } - } + ui.vertical_centered_justified(|ui| { + let width = ui.available_width(); + ui.columns(3, |columns| { + // Try to vertically center the horizontal layout + columns[1].horizontal(|ui| { + ui.add_space(width * 0.175 - 95.0); // These are magic numbers (eyeballed) + + let b = ui.button(text).on_hover_text_at_pointer(t!( + "tooltip.button.playpause", + lang + )); + + if (b.clicked() + || ui.input_mut(|i| { + i.consume_key(egui::Modifiers::NONE, egui::Key::Space) + })) + && !editor_focused + { + if self.tm.finished() { + self.tm = self.tm.restart(&self.code).unwrap(); + } else { + self.tm.paused = !self.tm.paused; + } + } + + if self.process_turing_controls(ui, &ctx, editor_focused, &lang) + { + ctx.request_repaint(); + if self.tm.is_inf_loop() { + warn!("Infinite loop detected!"); + self.infinite_loop_window = Some(Box::new( + InfiniteLoopWindow::new(&self.get_lang()), + )); + self.tm.paused = true; + } + } + }); + }); + }); }); self.tm.lang = self.get_lang(); diff --git a/src/windows/compsition_help_window.rs b/src/windows/compsition_help_window.rs index 096213d..8a25476 100644 --- a/src/windows/compsition_help_window.rs +++ b/src/windows/compsition_help_window.rs @@ -26,7 +26,7 @@ impl SecondaryWindow for CompositionHelpWindow { fn show(&self, ctx: &egui::Context) -> bool { let mut active = true; - egui::Window::new("List of composition libraries available") + egui::Window::new(t!("title.composition", self.lang)) .id(egui::Id::new("composition_help_window")) .resizable(false) .open(&mut active) @@ -43,27 +43,28 @@ impl SecondaryWindow for CompositionHelpWindow { .header(20.0, |mut header| { header.col(|ui| { ui.label( - RichText::new("Library name") // TODO: Translate - .heading(), + RichText::new(t!("lbl.composition.name", self.lang)).heading(), ) - .on_hover_text_at_pointer("The name to write to import it"); - // TODO: Translate + .on_hover_text_at_pointer(t!("tooltip.composition.name", self.lang)); }); header.col(|ui| { - ui.label(RichText::new("Description").heading()); // TODO: Translate + ui.label( + RichText::new(t!("lbl.composition.description", self.lang)) + .heading(), + ); }); header.col(|ui| { - ui.label(RichText::new("Initial state").heading()); // TODO: Translate + ui.label(RichText::new(t!("lbl.state.initial", self.lang)).heading()); }); header.col(|ui| { - ui.label(RichText::new("Final state").heading()); // TODO: Translate + ui.label(RichText::new(t!("lbl.state.final", self.lang)).heading()); }); header.col(|ui| { - ui.label(RichText::new("Used states").heading()); // TODO: Translate + ui.label(RichText::new(t!("lbl.state.used", self.lang)).heading()); }); }) .body(|mut body| { @@ -96,6 +97,10 @@ impl SecondaryWindow for CompositionHelpWindow { }); } }); + + ui.separator(); + + ui.label(RichText::new(t!("lbl.composition.help.txt", self.lang))); }); active diff --git a/src/windows/error_window.rs b/src/windows/error_window.rs index 63aaa9b..dc559d4 100644 --- a/src/windows/error_window.rs +++ b/src/windows/error_window.rs @@ -3,19 +3,24 @@ use std::path::PathBuf; use eframe; use eframe::egui::{self, RichText}; use eframe::epaint::Color32; +use internationalization::t; use turing_lib::{CompilerError, ErrorPosition}; +use crate::window::Language; + pub struct ErrorWindow { error: CompilerError, file: Option, line_msg: String, expected_msg: String, + lang: Language, } impl ErrorWindow { pub fn new( error: CompilerError, file: Option, + lang: Language, cc: &eframe::CreationContext<'_>, ) -> Self { let mut st = (*egui::Context::default().style()).clone(); @@ -42,29 +47,51 @@ impl ErrorWindow { file, line_msg, expected_msg, + lang, + } + } + + fn lang(&self) -> &str { + match self.lang { + Language::English => "en", + Language::Spanish => "es", } } } impl eframe::App for ErrorWindow { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + let lang = String::from(self.lang()); + egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { + ui.menu_button(t!("menu.language", self.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)); + }); + }); + egui::CentralPanel::default().show(ctx, |ui| { ui.vertical_centered_justified(|ui| { - let text = match self.file { - Some(ref file) => format!( - "Syntax error on file {:?}", - file.file_name() + Some(ref file) => { + let filename = file + .file_name() .unwrap_or(std::ffi::OsStr::new("User input")) - ), - None => "Syntax error".to_string(), + .to_str() + .unwrap(); + + t!( + "err.syntax", + file: filename, lang + ) + } + None => t!("err.syntax.simple", lang).to_string(), }; ui.label( RichText::new(text) - .color(Color32::LIGHT_RED) - .size(30.0) - .underline(), + .color(Color32::LIGHT_RED) + .size(30.0) + .underline(), ); }); @@ -104,8 +131,14 @@ impl eframe::App for ErrorWindow { "^", "~", width1 = position.start.1, - width2 = position.end.unwrap_or((0, position.start.1 +1)).1 - position.start.1, - width3 = self.error.code().len() - position.end.unwrap_or((0, position.start.1 +1)).1 + width2 = + position.end.unwrap_or((0, position.start.1 + 1)).1 + - position.start.1, + width3 = self.error.code().len() + - position + .end + .unwrap_or((0, position.start.1 + 1)) + .1 )) .color(Color32::RED) .size(20.0), @@ -120,13 +153,8 @@ impl eframe::App for ErrorWindow { }); }); }); - ui.label( - RichText::new( - "Could not initialize the Turing Machine. Please fix the syntax error and try again." - ) - .size(20.0) - ); - if ui.button("Close").clicked() { + ui.label(RichText::new(t!("err.initialization", lang)).size(20.0)); + if ui.button(t!("btn.close", lang)).clicked() { std::process::exit(2); } }); diff --git a/src/windows/workbook/book.rs b/src/windows/workbook/book.rs index 4c0bf51..fbf469e 100644 --- a/src/windows/workbook/book.rs +++ b/src/windows/workbook/book.rs @@ -1,4 +1,5 @@ use eframe::egui; +use internationalization::t; use serde::{self, Deserialize, Serialize}; use crate::windows::workbook::raw_data_to_image; @@ -54,14 +55,14 @@ impl BookWindow { let mut active = true; let mut code = None; - egui::Window::new("Workbook") //TODO: t!("title.debug", self.lang)) + egui::Window::new(t!("title.workbook", self.lang)) .id(egui::Id::new("exercises_window")) - .resizable(false) + .resizable(true) .open(&mut active) .show(ctx, |ui| { ui.horizontal_centered(|ui| { ui.vertical(|ui| { - ui.heading("Catalog"); //t!("title.exercises", self.lang)); + ui.heading(t!("heading.workbook.catalog", self.lang)); egui::ScrollArea::vertical().show(ui, |ui| { for (section, (title, exercises)) in self.exercises.iter().enumerate() { @@ -82,7 +83,7 @@ impl BookWindow { } }); - if ui.button("Load workbook").clicked() { + if ui.button(t!("btn.workbook.load", self.lang)).clicked() { if let Some(new_exercises) = load_workbook() { self.exercises = new_exercises; self.selected = (0, 0); @@ -108,7 +109,10 @@ impl BookWindow { ui.horizontal(|ui| { if ui - .add_enabled(self.selected.1 > 0, egui::Button::new("Previous")) + .add_enabled( + self.selected.1 > 0, + egui::Button::new(t!("btn.workbook.previous", self.lang)), + ) .clicked() { self.selected.1 -= 1; @@ -119,7 +123,7 @@ impl BookWindow { if ui .add_enabled( self.selected.1 < self.exercises[self.selected.0].1.len() - 1, - egui::Button::new("Next"), + egui::Button::new(t!("btn.workbook.next", self.lang)), ) .clicked() { @@ -127,7 +131,7 @@ impl BookWindow { } }); - if ui.button("Use this exercise").clicked() { + if ui.button(t!("btn.workbook.use", self.lang)).clicked() { code = Some(self.get_exercise(self.selected).code.clone()); } }); diff --git a/src/windows/workbook/wb_editor.rs b/src/windows/workbook/wb_editor.rs index 4928cf7..b84db7b 100644 --- a/src/windows/workbook/wb_editor.rs +++ b/src/windows/workbook/wb_editor.rs @@ -1,4 +1,5 @@ use eframe::egui; +use internationalization::t; use super::{ exercise::Exercise, load_image, save_workbook, Workbook, WorkbookChapter, MAX_IMG_SIZE, @@ -22,14 +23,16 @@ impl WorkbookEditorWindow { } pub fn set_lang(&mut self, lang: &str) { - self.lang = lang.to_string(); + self.lang = String::from(lang); } pub fn show(&mut self, ctx: &egui::Context) -> bool { // ctx.set_debug_on_hover(true); let mut active = true; - egui::Window::new("Workbook editor") //TODO: t!("title.debug", self.lang)) + let lang = &self.lang.clone(); + + egui::Window::new(t!("title.workbook.editor", lang)) .id(egui::Id::new("editor_window")) .resizable(true) .open(&mut active) @@ -38,7 +41,7 @@ impl WorkbookEditorWindow { ui.vertical(|ui| { ui.horizontal(|ui| { ui.vertical(|ui| { - ui.heading("Catalog"); //t!("title.exercises", self.lang)); + ui.heading(t!("heading.workbook.catalog", lang)); egui::ScrollArea::vertical() .id_source(egui::Id::new("scroll_list")) @@ -67,9 +70,13 @@ impl WorkbookEditorWindow { ui.separator(); - if ui.button("Add Exercise").clicked() { + if ui + .button(t!("btn.editor.add_exercise", lang)) + .clicked() + { exercises.push(WorkbookEditorWindow::new_exercise( exercises.len(), + lang, )); } }); @@ -78,10 +85,11 @@ impl WorkbookEditorWindow { ui.separator(); - if ui.button("Add Chapter").clicked() { + if ui.button(t!("btn.editor.add_chapter", lang)).clicked() { self.chapters.push(WorkbookEditorWindow::new_chapter( self.chapters.len(), 0, + lang, )); self.selected = (self.chapters.len() - 1, 0); } @@ -94,7 +102,7 @@ impl WorkbookEditorWindow { if let Some(ch) = self.chapters.get_mut(self.selected.0) { ui.add( egui::TextEdit::singleline(&mut ch.0) - .hint_text("Chapter title") + .hint_text(t!("tooltip.editor.chapter_title", lang)) .desired_width(0.0) .font(egui::TextStyle::Heading), ); @@ -105,7 +113,7 @@ impl WorkbookEditorWindow { if let Some(ex) = self.get_exercise(self.selected) { ui.add( egui::TextEdit::singleline(&mut ex.title) - .hint_text("Exercise title") + .hint_text(t!("tooltip.editor.chapter_title", lang)) .desired_width(0.0) .font(egui::TextStyle::Heading), ); @@ -128,7 +136,7 @@ impl WorkbookEditorWindow { ); ui.horizontal(|ui| { ui.add_space(15.0); - if ui.button("Add image").clicked() { + if ui.button(t!("btn.editor.add_image", lang)).clicked() { match load_image() { Some(img) => ex.set_cover(img), None => {} @@ -139,7 +147,7 @@ impl WorkbookEditorWindow { ui.add_space(50.0); - ui.label("Exercise code:"); //t!("editor.code.header", self.lang)); + ui.label(t!("lbl.editor.exercise_code", lang) + ":"); //t!("editor.code.header", self.lang)); let mut code = ex.code.clone(); egui::ScrollArea::vertical() @@ -159,7 +167,7 @@ impl WorkbookEditorWindow { }); ui.horizontal(|ui| { - if ui.button("Save workbook").clicked() { + if ui.button(t!("btn.editor.save_workbook", lang)).clicked() { save_workbook(&self.chapters); } }); @@ -169,16 +177,24 @@ impl WorkbookEditorWindow { active } - fn new_chapter(chapters_len: usize, exercises_len: usize) -> WorkbookChapter { + fn new_chapter(chapters_len: usize, exercises_len: usize, lang: &str) -> WorkbookChapter { ( - format!("New Chapter {}", chapters_len + 1), - vec![WorkbookEditorWindow::new_exercise(exercises_len)], + t!( + "lbl.editor.new_chapter", + num: (chapters_len + 1).to_string().as_str(), + lang + ), + vec![WorkbookEditorWindow::new_exercise(exercises_len, lang)], ) } - fn new_exercise(exercises_len: usize) -> Exercise { + fn new_exercise(exercises_len: usize, lang: &str) -> Exercise { Exercise::new( - &format!("New Exercise {}", exercises_len + 1), + &t!( + "lbl.editor.new_exercise", + num: (exercises_len + 1).to_string().as_str(), + lang + ), None, String::new(), )