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 179db4f2..a9c30259 100644 Binary files a/devtools/fonts/iced_devtools-icons.ttf and b/devtools/fonts/iced_devtools-icons.ttf differ diff --git a/devtools/src/icon.rs b/devtools/src/icon.rs index 27ceb9b7..63d32ecc 100644 --- a/devtools/src/icon.rs +++ b/devtools/src/icon.rs @@ -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, 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, }