Add Failed and Success states to tester devtool

This commit is contained in:
Héctor Ramón Jiménez 2025-06-04 22:40:32 +02:00
parent 73f5569f28
commit e548372fe0
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
7 changed files with 198 additions and 53 deletions

37
Cargo.lock generated
View file

@ -117,9 +117,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]]
name = "anstyle"
version = "1.0.10"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
[[package]]
name = "anyhow"
@ -2342,9 +2342,9 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.13"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8"
checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb"
dependencies = [
"base64 0.22.1",
"bytes",
@ -4613,9 +4613,9 @@ dependencies = [
[[package]]
name = "read-fonts"
version = "0.29.2"
version = "0.29.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f96bfbb7df43d34a2b7b8582fcbcb676ba02a763265cb90bc8aabfd62b57d64"
checksum = "04ca636dac446b5664bd16c069c00a9621806895b8bb02c2dc68542b23b8f25d"
dependencies = [
"bytemuck",
"font-types",
@ -5950,9 +5950,9 @@ dependencies = [
[[package]]
name = "tower-http"
version = "0.6.5"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cc2d9e086a412a451384326f521c8123a99a466b329941a9403696bff9b0da2"
checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
dependencies = [
"bitflags 2.9.1",
"bytes",
@ -6938,13 +6938,13 @@ checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
[[package]]
name = "windows-registry"
version = "0.4.0"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820"
dependencies = [
"windows-link",
"windows-result 0.3.4",
"windows-strings 0.3.1",
"windows-targets 0.53.0",
"windows-strings 0.4.2",
]
[[package]]
@ -6984,15 +6984,6 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-strings"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.4.2"
@ -7642,9 +7633,9 @@ dependencies = [
[[package]]
name = "zune-jpeg"
version = "0.4.14"
version = "0.4.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028"
checksum = "3e4a518c0ea2576f4da876349d7f67a7be489297cd77c2cf9e04c2e05fcd3974"
dependencies = [
"zune-core",
]

View file

@ -3,6 +3,10 @@ module = "icon"
[glyphs]
play = "entypo-play"
stop = "entypo-stop"
pause = "entypo-pause"
record = "entypo-record"
import = "entypo-folder"
export = "entypo-floppy"
lightbulb = "fontawesome-lightbulb"
check = "entypo-check"
cancel = "entypo-cancel"
folder = "entypo-folder"
floppy = "entypo-floppy"

View file

@ -5,6 +5,54 @@ use crate::widget::{Text, text};
pub const FONT: &[u8] = include_bytes!("../fonts/iced_devtools-icons.ttf");
pub fn cancel<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer>
where
Theme: text::Catalog + 'a,
Renderer: program::Renderer,
{
icon("\u{2715}")
}
pub fn check<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer>
where
Theme: text::Catalog + 'a,
Renderer: program::Renderer,
{
icon("\u{2713}")
}
pub fn floppy<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer>
where
Theme: text::Catalog + 'a,
Renderer: program::Renderer,
{
icon("\u{1F4BE}")
}
pub fn folder<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer>
where
Theme: text::Catalog + 'a,
Renderer: program::Renderer,
{
icon("\u{1F4C1}")
}
pub fn lightbulb<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer>
where
Theme: text::Catalog + 'a,
Renderer: program::Renderer,
{
icon("\u{F0EB}")
}
pub fn pause<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer>
where
Theme: text::Catalog + 'a,
Renderer: program::Renderer,
{
icon("\u{2389}")
}
pub fn play<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer>
where
Theme: text::Catalog + 'a,
@ -29,6 +77,14 @@ where
icon("\u{25A0}")
}
pub fn tape<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer>
where
Theme: text::Catalog + 'a,
Renderer: program::Renderer,
{
icon("\u{2707}")
}
fn icon<'a, Theme, Renderer>(codepoint: &'a str) -> Text<'a, Theme, Renderer>
where
Theme: text::Catalog + 'a,

View file

@ -36,12 +36,22 @@ enum State<P: Program> {
Recording {
state: P::State,
},
Ready {
state: P::State,
},
Playing {
emulator: Emulator<P>,
current: usize,
outcome: Outcome,
},
}
enum Outcome {
Running,
Failed,
Success,
}
#[derive(Debug, Clone)]
pub enum Message {
ChangeViewport(Size),
@ -83,7 +93,14 @@ impl<P: Program + 'static> Tester<P> {
}
pub fn is_busy(&self) -> bool {
matches!(self.state, State::Recording { .. } | State::Playing { .. })
matches!(
self.state,
State::Recording { .. }
| State::Playing {
outcome: Outcome::Running,
..
}
)
}
pub fn update(&mut self, program: &P, message: Message) -> Task<Tick<P>> {
@ -117,7 +134,13 @@ impl<P: Program + 'static> Tester<P> {
task.map(Tick::Program)
}
Message::Stop => {
self.state = State::Idle;
let State::Recording { state } =
std::mem::replace(&mut self.state, State::Idle)
else {
return Task::none();
};
self.state = State::Ready { state };
Task::none()
}
@ -135,6 +158,7 @@ impl<P: Program + 'static> Tester<P> {
self.state = State::Playing {
emulator,
current: 0,
outcome: Outcome::Running,
};
Task::run(receiver, Tick::Emulator)
@ -190,7 +214,11 @@ impl<P: Program + 'static> Tester<P> {
Task::none()
}
Tick::Emulator(event) => {
let State::Playing { emulator, current } = &mut self.state
let State::Playing {
emulator,
current,
outcome,
} = &mut self.state
else {
return Task::none();
};
@ -199,6 +227,9 @@ impl<P: Program + 'static> Tester<P> {
emulator::Event::Action(action) => {
emulator.perform(program, action);
}
emulator::Event::Failed => {
*outcome = Outcome::Failed;
}
emulator::Event::Ready => {
if let Some(instruction) =
self.instructions.get(*current).cloned()
@ -206,6 +237,10 @@ impl<P: Program + 'static> Tester<P> {
emulator.run(program, instruction);
*current += 1;
}
if *current >= self.instructions.len() {
*outcome = Outcome::Success;
}
}
}
@ -216,7 +251,9 @@ impl<P: Program + 'static> Tester<P> {
pub fn subscription(&self, program: &P) -> Subscription<Tick<P>> {
match &self.state {
State::Idle | State::Playing { .. } => Subscription::none(),
State::Idle | State::Playing { .. } | State::Ready { .. } => {
Subscription::none()
}
State::Recording { state } => {
program.subscription(state).map(Tick::Program)
}
@ -230,22 +267,44 @@ impl<P: Program + 'static> Tester<P> {
current: impl FnOnce() -> Element<'a, T, Theme, P::Renderer>,
emulate: impl Fn(Tick<P>) -> T + 'a,
) -> Element<'a, T, Theme, P::Renderer> {
let status = match &self.state {
State::Idle => monospace("Idle").style(|theme| text::Style {
color: Some(
theme.extended_palette().background.strongest.color,
),
}),
State::Recording { .. } => {
monospace("Recording").style(|theme| text::Style {
color: Some(theme.palette().danger),
let status = {
let (icon, label) = match &self.state {
State::Idle => (text(""), "Idle"),
State::Recording { .. } => (icon::record(), "Recording"),
State::Ready { .. } => (icon::lightbulb(), "Ready"),
State::Playing { outcome, .. } => match outcome {
Outcome::Running => (icon::play(), "Playing"),
Outcome::Failed => (icon::cancel(), "Failed"),
Outcome::Success => (icon::check(), "Success"),
},
};
container(row![icon.size(14), label].align_y(Center).spacing(8))
.style(|theme: &Theme| {
let palette = theme.extended_palette();
container::Style {
text_color: Some(match &self.state {
State::Idle => palette.background.strongest.color,
State::Recording { .. } => {
palette.danger.base.color
}
State::Ready { .. } => palette.warning.base.color,
State::Playing { outcome, .. } => match outcome {
Outcome::Running => theme.palette().primary,
Outcome::Failed => theme.palette().danger,
Outcome::Success => {
theme
.extended_palette()
.success
.strong
.color
}
},
}),
..container::Style::default()
}
})
}
State::Playing { .. } => {
monospace("Playing").style(|theme| text::Style {
color: Some(theme.palette().primary),
})
}
};
let viewport = container(
@ -263,6 +322,13 @@ impl<P: Program + 'static> Tester<P> {
)
.map(emulate)
}
State::Ready { state } => {
let theme = program.theme(state, window);
let view =
program.view(state, window).map(Tick::Program);
Element::from(themer(theme, view)).map(emulate)
}
State::Playing { emulator, .. } => {
let theme = emulator.theme(program);
let view = emulator.view(program).map(Tick::Program);
@ -285,7 +351,12 @@ impl<P: Program + 'static> Tester<P> {
border: border::width(2.0).color(match &self.state {
State::Idle => palette.background.strongest.color,
State::Recording { .. } => palette.danger.base.color,
State::Playing { .. } => palette.primary.base.color,
State::Ready { .. } => palette.warning.weak.color,
State::Playing { outcome, .. } => match outcome {
Outcome::Running => palette.primary.base.color,
Outcome::Failed => palette.danger.strong.color,
Outcome::Success => palette.success.strong.color,
},
}),
..container::Style::default()
}
@ -305,7 +376,7 @@ impl<P: Program + 'static> Tester<P> {
width: width.parse().unwrap_or(self.viewport.width),
..self.viewport
})),
monospace("x"),
monospace("x").size(14),
text_input("Height", &self.viewport.height.to_string())
.size(14)
.on_input(|height| Message::ChangeViewport(Size {
@ -318,7 +389,7 @@ impl<P: Program + 'static> Tester<P> {
let preset = combo_box(
&self.presets,
"Default Preset",
"Default",
self.preset.as_ref(),
Message::PresetSelected,
)
@ -349,9 +420,32 @@ impl<P: Program + 'static> Tester<P> {
.size(10)
.style(move |theme: &Theme| text::Style {
color: match &self.state {
State::Playing { current, .. } => {
State::Playing {
current,
outcome,
..
} => {
if *current == i {
Some(theme.palette().primary)
Some(match outcome {
Outcome::Running => {
theme.palette().primary
}
Outcome::Failed => {
theme
.extended_palette()
.danger
.strong
.color
}
Outcome::Success => {
theme
.extended_palette()
.success
.strong
.color
}
})
} else if *current > i {
Some(
theme
@ -394,8 +488,7 @@ impl<P: Program + 'static> Tester<P> {
} else {
button(icon::record().size(14).width(Fill).center())
.on_press_maybe(
matches!(self.state, State::Idle)
.then_some(Message::Record),
(!self.is_busy()).then_some(Message::Record),
)
.style(button::danger)
}

View file

@ -588,10 +588,10 @@ fn presets() -> impl Iterator<Item = iced::application::Preset<Todos, Message>>
Command::done(Message::Loaded(Err(LoadError::File))),
)
}),
Preset::new("Basic", || {
Preset::new("Carl Sagan", || {
(
Todos::Loaded(State {
input_value: "Bake an apple pie".to_owned(),
input_value: "Make an apple pie".to_owned(),
filter: Filter::All,
tasks: vec![Task {
id: Uuid::new_v4(),

View file

@ -34,6 +34,7 @@ pub struct Emulator<P: Program> {
#[allow(missing_debug_implementations)]
pub enum Event<P: Program> {
Action(Action<P::Message>),
Failed,
Ready,
}