Implement import/export for tester devtool

This commit is contained in:
Héctor Ramón Jiménez 2025-06-05 00:23:36 +02:00
parent e548372fe0
commit 1fd6980f91
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
6 changed files with 287 additions and 45 deletions

149
Cargo.lock generated
View file

@ -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",

View file

@ -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"

View file

@ -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

View file

@ -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<String>),
}
#[allow(missing_debug_implementations)]
pub enum Tick<P: Program> {
Tester(Message),
Program(P::Message),
Recorder(Event),
Emulator(emulator::Event<P>),
@ -163,6 +168,67 @@ impl<P: Program + 'static> Tester<P> {
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<Vec<_>, _> =
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<P: Program + 'static> Tester<P> {
pub fn tick(&mut self, program: &P, tick: Tick<P>) -> Task<Tick<P>> {
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<P: Program + 'static> Tester<P> {
.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)
};

View file

@ -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)

View file

@ -10,6 +10,12 @@ pub enum Instruction {
Interact(Interaction),
}
impl Instruction {
pub fn parse(line: &str) -> Result<Self, ParseError> {
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);