From 1fd6980f91510363f3288b0b306aa5ab43d31598 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 5 Jun 2025 00:23:36 +0200 Subject: [PATCH] Implement import/export for `tester` devtool --- Cargo.lock | 149 ++++++++++++++++++++++++---- Cargo.toml | 1 + devtools/Cargo.toml | 5 +- devtools/src/tester.rs | 123 +++++++++++++++++++---- examples/todos/tests/carl_sagan.ice | 8 ++ test/src/instruction.rs | 46 ++++++++- 6 files changed, 287 insertions(+), 45 deletions(-) create mode 100644 examples/todos/tests/carl_sagan.ice diff --git a/Cargo.lock b/Cargo.lock index cbe3b805..fe774c93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -205,6 +205,25 @@ dependencies = [ "zbus", ] +[[package]] +name = "ashpd" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" +dependencies = [ + "async-fs", + "async-net", + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.9.1", + "raw-window-handle 0.6.2", + "serde", + "serde_repr", + "url", + "zbus", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -585,6 +604,15 @@ dependencies = [ "objc2 0.5.2", ] +[[package]] +name = "block2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2" +dependencies = [ + "objc2 0.6.1", +] + [[package]] name = "blocking" version = "1.6.1" @@ -850,7 +878,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b7f4aaa047ba3c3630b080bb9860894732ff23e2aee290a418909aa6d5df38f" dependencies = [ "objc2 0.5.2", - "objc2-app-kit", + "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", ] @@ -1207,7 +1235,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18e1a09f280e29a8b00bc7e81eca5ac87dca0575639c9422a5fa25a07bb884b8" dependencies = [ - "ashpd", + "ashpd 0.10.3", "async-std", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -1273,6 +1301,28 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "dispatch2" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a0d569e003ff27784e0e14e4a594048698e0c0f0b66cabcb51511be55a7caa0" +dependencies = [ + "bitflags 2.9.1", + "block2 0.6.1", + "libc", + "objc2 0.6.1", +] + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.9.1", + "objc2 0.6.1", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1365,7 +1415,7 @@ name = "editor" version = "0.1.0" dependencies = [ "iced", - "rfd", + "rfd 0.13.0", "tokio", ] @@ -2461,6 +2511,7 @@ dependencies = [ "iced_test", "iced_widget", "log", + "rfd 0.15.3", ] [[package]] @@ -3646,7 +3697,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ "bitflags 2.9.1", - "block2", + "block2 0.5.1", "libc", "objc2 0.5.2", "objc2-core-data", @@ -3655,6 +3706,18 @@ dependencies = [ "objc2-quartz-core", ] +[[package]] +name = "objc2-app-kit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" +dependencies = [ + "bitflags 2.9.1", + "block2 0.6.1", + "objc2 0.6.1", + "objc2-foundation 0.3.1", +] + [[package]] name = "objc2-cloud-kit" version = "0.2.2" @@ -3662,7 +3725,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ "bitflags 2.9.1", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", "objc2-foundation 0.2.2", @@ -3674,7 +3737,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", ] @@ -3686,18 +3749,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ "bitflags 2.9.1", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", ] +[[package]] +name = "objc2-core-foundation" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +dependencies = [ + "bitflags 2.9.1", + "dispatch2 0.3.0", + "objc2 0.6.1", +] + [[package]] name = "objc2-core-image" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", "objc2-metal", @@ -3709,7 +3783,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-contacts", "objc2-foundation 0.2.2", @@ -3728,7 +3802,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ "bitflags 2.9.1", - "block2", + "block2 0.5.1", "dispatch", "libc", "objc2 0.5.2", @@ -3742,6 +3816,7 @@ checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" dependencies = [ "bitflags 2.9.1", "objc2 0.6.1", + "objc2-core-foundation", ] [[package]] @@ -3750,9 +3825,9 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.2", - "objc2-app-kit", + "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", ] @@ -3763,7 +3838,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ "bitflags 2.9.1", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", ] @@ -3775,7 +3850,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ "bitflags 2.9.1", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", "objc2-metal", @@ -3798,7 +3873,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ "bitflags 2.9.1", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-cloud-kit", "objc2-core-data", @@ -3818,7 +3893,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", ] @@ -3830,7 +3905,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ "bitflags 2.9.1", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", "objc2-foundation 0.2.2", @@ -4291,6 +4366,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + [[package]] name = "potential_utf" version = "0.1.2" @@ -4769,6 +4850,30 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rfd" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80c844748fdc82aae252ee4594a89b6e7ebef1063de7951545564cbc4e57075d" +dependencies = [ + "ashpd 0.11.0", + "block2 0.6.1", + "dispatch2 0.2.0", + "js-sys", + "log", + "objc2 0.6.1", + "objc2-app-kit 0.3.1", + "objc2-core-foundation", + "objc2-foundation 0.3.1", + "pollster", + "raw-window-handle 0.6.2", + "urlencoding", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.59.0", +] + [[package]] name = "rgb" version = "0.8.50" @@ -6195,6 +6300,12 @@ dependencies = [ "iced", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "usvg" version = "0.42.0" @@ -7280,7 +7391,7 @@ dependencies = [ "android-activity", "atomic-waker", "bitflags 2.9.1", - "block2", + "block2 0.5.1", "bytemuck", "calloop", "cfg_aliases", @@ -7294,7 +7405,7 @@ dependencies = [ "memmap2", "ndk", "objc2 0.5.2", - "objc2-app-kit", + "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", "objc2-ui-kit", "orbclient", diff --git a/Cargo.toml b/Cargo.toml index dbfe4cc5..54c10460 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -193,6 +193,7 @@ pulldown-cmark = "0.12" qrcode = { version = "0.13", default-features = false } raw-window-handle = "0.6" resvg = "0.42" +rfd = "0.15" rustc-hash = "2.0" serde = "1.0" semver = "1.0" diff --git a/devtools/Cargo.toml b/devtools/Cargo.toml index 98f83745..8e50978b 100644 --- a/devtools/Cargo.toml +++ b/devtools/Cargo.toml @@ -15,7 +15,7 @@ workspace = true [features] time-travel = ["iced_program/time-travel"] -tester = ["dep:iced_test"] +tester = ["dep:iced_test", "dep:rfd"] [dependencies] iced_debug.workspace = true @@ -25,3 +25,6 @@ log.workspace = true iced_test.workspace = true iced_test.optional = true + +rfd.workspace = true +rfd.optional = true diff --git a/devtools/src/tester.rs b/devtools/src/tester.rs index 83358317..ac52b5c4 100644 --- a/devtools/src/tester.rs +++ b/devtools/src/tester.rs @@ -9,6 +9,7 @@ use crate::core::alignment::Horizontal::Right; use crate::core::border; use crate::core::window; use crate::core::{Element, Event, Size, Theme}; +use crate::executor; use crate::futures::Subscription; use crate::futures::futures::channel::mpsc; use crate::icon; @@ -60,10 +61,14 @@ pub enum Message { Record, Stop, Play, + Import, + Export, + Imported(Option), } #[allow(missing_debug_implementations)] pub enum Tick { + Tester(Message), Program(P::Message), Recorder(Event), Emulator(emulator::Event

), @@ -163,6 +168,67 @@ impl Tester

{ Task::run(receiver, Tick::Emulator) } + Message::Import => { + use std::fs; + + let import = rfd::AsyncFileDialog::new() + .add_filter("ice", &["ice"]) + .pick_file(); + + Task::future(import) + .and_then(|file| { + executor::spawn_blocking(move |mut sender| { + let _ = sender + .try_send(fs::read_to_string(file.path()).ok()); + }) + }) + .map(Message::Imported) + .map(Tick::Tester) + } + Message::Export => { + use std::fs; + use std::thread; + + let test: Vec<_> = self + .instructions + .iter() + .map(Instruction::to_string) + .collect(); + + let export = rfd::AsyncFileDialog::new() + .add_filter("ice", &["ice"]) + .save_file(); + + Task::future(async move { + let Some(file) = export.await else { + return; + }; + + let _ = thread::spawn(move || { + fs::write(file.path(), test.join("\n")) + }); + }) + .discard() + } + Message::Imported(instructions) => { + let Some(instructions) = instructions else { + return Task::none(); + }; + + let instructions: Result, _> = + instructions.lines().map(Instruction::parse).collect(); + + match instructions { + Ok(instructions) => { + self.instructions = instructions; + } + Err(error) => { + log::error!("{error}"); + } + } + + Task::none() + } } } @@ -180,6 +246,7 @@ impl Tester

{ pub fn tick(&mut self, program: &P, tick: Tick

) -> Task> { match tick { + Tick::Tester(message) => self.update(program, message), Tick::Program(message) => { let State::Recording { state } = &mut self.state else { return Task::none(); @@ -473,29 +540,43 @@ impl Tester

{ .height(Fill) .padding(5); - let controls = { - row![ - button(icon::play().size(14).width(Fill).center()) - .on_press_maybe( - (!matches!(self.state, State::Recording { .. }) - && !self.instructions.is_empty()) - .then_some(Message::Play), - ), - if let State::Recording { .. } = &self.state { - button(icon::stop().size(14).width(Fill).center()) - .on_press(Message::Stop) - .style(button::success) - } else { - button(icon::record().size(14).width(Fill).center()) - .on_press_maybe( - (!self.is_busy()).then_some(Message::Record), - ) - .style(button::danger) - } - ] - .spacing(10) + let control = |icon: text::Text<'static, _, _>| { + button(icon.size(14).width(Fill).height(Fill).center()) }; + let play = control(icon::play()).on_press_maybe( + (!matches!(self.state, State::Recording { .. }) + && !self.instructions.is_empty()) + .then_some(Message::Play), + ); + + let record = if let State::Recording { .. } = &self.state { + control(icon::stop()) + .on_press(Message::Stop) + .style(button::success) + } else { + control(icon::record()) + .on_press_maybe( + (!self.is_busy()).then_some(Message::Record), + ) + .style(button::danger) + }; + + let import = control(icon::folder()) + .on_press_maybe((!self.is_busy()).then_some(Message::Import)) + .style(button::secondary); + + let export = control(icon::floppy()) + .on_press_maybe( + (!matches!(self.state, State::Recording { .. }) + && !self.instructions.is_empty()) + .then_some(Message::Export), + ) + .style(button::success); + + let controls = + row![import, export, play, record].height(30).spacing(10); + column![instructions, controls].spacing(10).align_x(Center) }; diff --git a/examples/todos/tests/carl_sagan.ice b/examples/todos/tests/carl_sagan.ice new file mode 100644 index 00000000..d536b9f1 --- /dev/null +++ b/examples/todos/tests/carl_sagan.ice @@ -0,0 +1,8 @@ +click left at (377.80, 236.50) +type "Create the universe" +type enter +type "Make an apple pie" +type enter +click left at (135.40, 351.70) +click left at (153.80, 398.10) +move cursor to (511.40, 448.50) \ No newline at end of file diff --git a/test/src/instruction.rs b/test/src/instruction.rs index d1323bcf..fd9ba2c9 100644 --- a/test/src/instruction.rs +++ b/test/src/instruction.rs @@ -10,6 +10,12 @@ pub enum Instruction { Interact(Interaction), } +impl Instruction { + pub fn parse(line: &str) -> Result { + parser::run(line) + } +} + impl fmt::Display for Instruction { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -331,15 +337,16 @@ mod format { } } -pub use parser::{Error as ParseError, run as parse}; +pub use parser::Error as ParseError; mod parser { use super::*; use nom::branch::alt; use nom::bytes::complete::tag; - use nom::character::complete::{char, multispace0}; - use nom::combinator::{map, opt}; + use nom::character::complete::{char, multispace0, satisfy}; + use nom::combinator::{map, opt, recognize}; + use nom::multi::many0; use nom::number::float; use nom::sequence::{delimited, preceded, separated_pair}; use nom::{Finish, IResult, Parser}; @@ -360,7 +367,11 @@ mod parser { } fn interaction(input: &str) -> IResult<&str, Interaction> { - map(mouse, Interaction::Mouse).parse(input) + alt(( + map(mouse, Interaction::Mouse), + map(keyboard, Interaction::Keyboard), + )) + .parse(input) } fn mouse(input: &str) -> IResult<&str, Mouse> { @@ -395,6 +406,33 @@ mod parser { .parse(input) } + fn keyboard(input: &str) -> IResult<&str, Keyboard> { + alt(( + map(preceded(tag("type "), string), Keyboard::Typewrite), + map(preceded(tag("type "), key), Keyboard::Type), + )) + .parse(input) + } + + fn key(input: &str) -> IResult<&str, Key> { + alt(( + map(tag("enter"), |_| Key::Enter), + map(tag("escape"), |_| Key::Escape), + map(tag("tab"), |_| Key::Tab), + map(tag("backspace"), |_| Key::Backspace), + )) + .parse(input) + } + + fn string(input: &str) -> IResult<&str, String> { + delimited( + char('"'), + map(recognize(many0(satisfy(|c| c != '"'))), str::to_owned), + char('"'), + ) + .parse(input) + } + fn point(input: &str) -> IResult<&str, Point> { let comma = (multispace0, char(','), multispace0);