From e548372fe042a1f4aa911ce279e839fbc8f63c64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 4 Jun 2025 22:40:32 +0200 Subject: [PATCH] Add `Failed` and `Success` states to `tester` devtool --- Cargo.lock | 37 +++--- devtools/fonts/iced_devtools-icons.toml | 8 +- devtools/fonts/iced_devtools-icons.ttf | Bin 5888 -> 7132 bytes devtools/src/icon.rs | 56 +++++++++ devtools/src/tester.rs | 145 +++++++++++++++++++----- examples/todos/src/main.rs | 4 +- test/src/emulator.rs | 1 + 7 files changed, 198 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 38d0f64b..cbe3b805 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/devtools/fonts/iced_devtools-icons.toml b/devtools/fonts/iced_devtools-icons.toml index 69c8d86d..9444ede4 100644 --- a/devtools/fonts/iced_devtools-icons.toml +++ b/devtools/fonts/iced_devtools-icons.toml @@ -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" diff --git a/devtools/fonts/iced_devtools-icons.ttf b/devtools/fonts/iced_devtools-icons.ttf index 179db4f20f63ee12518bfbe6f661c660b1a8fb58..a9c302594e70ecc73a200689d1148fe3e7e6cc76 100644 GIT binary patch delta 1823 zcmaJ?Uu=_A6hG&FfBJ{^>(_5z|94&2c5SmQW$C(3wpl8oC=T7ipKU;ZuIpB$W6d@e zX3R?BcwmWUIvxlh62pTDJS;#6F~p!*3^B&=VD>$&Hi z-#z!-bI(2J_EhX)`@Fw2eJx`Lz#afNk{!(ymm~Xjk-P}NOxdYPKjGBOA?l>z;>hS= z?Ad!HCrC7nWG0G)%Orb=>qiRH!#nTK)BtdI0Qlv6E;H1$TwbH8voxQ~6QJwj_LKZQ z$)Wt{%edoFv;vGbf9vN}rP4LbCsOW;A#HSN$WBduWs1 z;@HIG+|>C20MiwkKT#ad6=!aB-U9HXKmPjwUR&6MU3~CNxx8=|H<#uCNef^OxXP%R zxRDM!4?G+qT&9l$2%c%U+;A&=HBu9?J^iCx{x76rXQRW)N^N!ZBa^_$5)T51pd(S# zR2Lf9%0dg(iWKW;!tt2`9E9b?wP` z9a3jkEr?4>D6Y)TDse@Y=jLRY2AUL7=Dtzl0r}jy3Uu*OMS-M1T)9-aj`AW)aVEO( z2poWY>D>ZF7I(e*S~4CJO9Icj>DGA!{uJ;6^1_S`1(AL;Ad0gTd5-D1qvSKP0bmR> zfDC*4`(8<>UheF0J3^{r5Is#uzK7cr;b4G*)f)GqBN1Z(iRsijW_PqE+f#Iqc2!4+ z-KK7kSPDuk9MM*`huf+YV-XsrI3L3cCX>XvYIP>2H+YlH{oz!1cXuj`VXO5GT}i(~ zw2R>#TU-G@=Qf*N`jBf=EZ$;phfwE|BsUBAT^;eBd_Mnnl3Ddequ%8;%0{KJHo86L zbjG$vw=|cOn!UZfdquawp?qoSkyX;H{I;s z+Sh(06=Knjp=!35f8hM=37EkF9(X)Sx=G9tVk)v1Y{!=S)LdAhza~* ziZ=gF$w{w=)8q8GUG^H;LRL%(z36B{hgB7W^?@+uaTRmMwLYGV*n(~BlaD$Y z*S?5$bwzPyz35Z1S@p$-*prKEKWiZCZ|qqA@%eMr=X-eX9y!3u>*VBPa#8{x4865z z?&(hl$wQti6$SE*k`Ye<;wXcqf&QpXgC}y`tX&dte;S z!V;CsbqdchLdgqym=9{t9yJCEw8Dop8j-3`ClK<}jqCgvx7$dC!_iU!MS4Z_Xh0)w z0-=~WF_GgZCdZ0=v5=V-$8*`S@gX6b&t;E^+05~5u3#t}9m!7)o+u28!-cV8aatT6 OD-7kv;YOE!N%|MgA5hc) delta 554 zcmY*WO(;ZB6h8OeYmA9uArxwq8TpBgNG5+9l0xJ+@w^%H7W2$)W|E)$>})6+D_JOZ zW1-ZnCTYUL(%Oy%D+-jX*SY6>-}%0K?tAa9d82;Ev%Zqqyd`1-L^NYqx{S3> z)B>L(4vdAk2RxRu2b+6NhOF88&t>2-z}%1?li&-0JCrCCUYdKZ>h2@r2)Z_ddZ6~I zy_`rC0ymi;vi*D%SoJGSD?YyHdO=CTJ}4Y9blRjY;0=&2OJ9_far=R{fIaiN72Lmf zJ^`O#$e@hG;v><_F=nGpZ892^YpK>0QBh#`W0Iv4!^|(dhrVQDk2MUL#FUI?iQ82j zvI{S2lAtE=PCOcs_t2a2CBMG@R<@7X3N?yB$B`UIof=ctTwBm+@3@(={)8Ij#f4O; z!+pu0Af6}_H#Hz3IB0n-pU(HcQ$BwLLl0A{=YKPQ)Y6FyXK}yXg$AB~ODLa)pTgdd z)4#n+d`SbfQVYuab!p_F3fcx&eb$?^!b=6y7-xcIk%+}3Qpm8r() -> 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, diff --git a/devtools/src/tester.rs b/devtools/src/tester.rs index 5831a13f..83358317 100644 --- a/devtools/src/tester.rs +++ b/devtools/src/tester.rs @@ -36,12 +36,22 @@ enum State { Recording { state: P::State, }, + Ready { + state: P::State, + }, Playing { emulator: Emulator

, current: usize, + outcome: Outcome, }, } +enum Outcome { + Running, + Failed, + Success, +} + #[derive(Debug, Clone)] pub enum Message { ChangeViewport(Size), @@ -83,7 +93,14 @@ impl Tester

{ } 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> { @@ -117,7 +134,13 @@ impl Tester

{ 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 Tester

{ self.state = State::Playing { emulator, current: 0, + outcome: Outcome::Running, }; Task::run(receiver, Tick::Emulator) @@ -190,7 +214,11 @@ impl Tester

{ 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 Tester

{ 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 Tester

{ emulator.run(program, instruction); *current += 1; } + + if *current >= self.instructions.len() { + *outcome = Outcome::Success; + } } } @@ -216,7 +251,9 @@ impl Tester

{ pub fn subscription(&self, program: &P) -> Subscription> { 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 Tester

{ current: impl FnOnce() -> Element<'a, T, Theme, P::Renderer>, emulate: impl Fn(Tick

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

{ ) .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 Tester

{ 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 Tester

{ 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 Tester

{ let preset = combo_box( &self.presets, - "Default Preset", + "Default", self.preset.as_ref(), Message::PresetSelected, ) @@ -349,9 +420,32 @@ impl Tester

{ .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 Tester

{ } 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) } diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index dc6dabbf..2eb65157 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -588,10 +588,10 @@ fn presets() -> impl Iterator> 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(), diff --git a/test/src/emulator.rs b/test/src/emulator.rs index 98841e57..fa1c4b3d 100644 --- a/test/src/emulator.rs +++ b/test/src/emulator.rs @@ -34,6 +34,7 @@ pub struct Emulator { #[allow(missing_debug_implementations)] pub enum Event { Action(Action), + Failed, Ready, }