From ed528c9c5394bef9c344f967ec72d59d7137d6ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 31 May 2025 05:50:25 +0200 Subject: [PATCH] Plug `Emulator` into `devtools` :tada: --- devtools/src/lib.rs | 172 +++++++++++++++++++++++++++++++++-------- futures/src/runtime.rs | 11 ++- test/src/emulator.rs | 6 ++ test/src/lib.rs | 1 + 4 files changed, 158 insertions(+), 32 deletions(-) diff --git a/devtools/src/lib.rs b/devtools/src/lib.rs index e8dcfa71..d516d075 100644 --- a/devtools/src/lib.rs +++ b/devtools/src/lib.rs @@ -25,6 +25,8 @@ use crate::futures::Subscription; use crate::program::Program; use crate::runtime::Task; use crate::runtime::font; +use crate::test::Emulator; +use crate::test::emulator; use crate::test::instruction; use crate::time_machine::TimeMachine; use crate::widget::{ @@ -126,7 +128,7 @@ where { state: P::State, size: Size, - mode: Mode, + mode: Mode

, show_notification: bool, time_machine: TimeMachine

, } @@ -145,17 +147,27 @@ pub enum Message { Record, Stop, Recorded(core::Event), + Play, } -enum Mode { +enum Mode { Hidden, - Open { recorder: Recorder }, + Open { recorder: Recorder

}, Setup(Setup), } -struct Recorder { +struct Recorder { instructions: Vec, - is_recording: bool, + state: State

, +} + +enum State { + Idle, + Recording, + Playing { + emulator: Emulator

, + current: usize, + }, } enum Setup { @@ -207,11 +219,16 @@ where self.mode = Mode::Open { recorder: Recorder { instructions: Vec::new(), - is_recording: false, + state: State::Idle, }, }; } - Mode::Open { recorder } if !recorder.is_recording => { + Mode::Open { + recorder: + Recorder { + state: State::Idle, .. + }, + } => { self.mode = Mode::Hidden; } Mode::Setup(_) | Mode::Open { .. } => {} @@ -321,7 +338,7 @@ where }; recorder.instructions.clear(); - recorder.is_recording = true; + recorder.state = State::Recording; let (state, task) = program.boot(); self.state = state; @@ -367,10 +384,27 @@ where return Task::none(); }; - recorder.is_recording = false; + recorder.state = State::Idle; Task::none() } + Message::Play => { + let Mode::Open { recorder } = &mut self.mode else { + return Task::none(); + }; + + let (sender, receiver) = + futures::futures::channel::mpsc::channel(1); + + let emulator = Emulator::new(program, self.size, sender); + + recorder.state = State::Playing { + emulator, + current: 0, + }; + + Task::run(receiver, Event::Emulator) + } }, Event::Program(message) => { self.time_machine.push(&message); @@ -402,6 +436,34 @@ where Task::none() } + Event::Emulator(event) => { + let Mode::Open { + recorder: + Recorder { + state: State::Playing { emulator, current }, + instructions, + }, + } = &mut self.mode + else { + return Task::none(); + }; + + match event { + emulator::Event::Action(action) => { + emulator.perform(program, action); + } + emulator::Event::Ready => { + if let Some(instruction) = + instructions.get(*current).cloned() + { + emulator.run(program, instruction); + *current += 1; + } + } + } + + Task::none() + } Event::Discard => Task::none(), } } @@ -414,7 +476,17 @@ where let state = self.state(); let view = { - let view = program.view(state, window); + let view = match &self.mode { + Mode::Open { + recorder: + Recorder { + state: State::Playing { emulator, .. }, + .. + }, + } => emulator.view(program), + _ => program.view(state, window), + }; + let theme = program.theme(state, window); let view: Element<'_, _, Theme, _> = themer(theme, view).into(); @@ -477,10 +549,34 @@ where )) } else { scrollable( - column(recorder.instructions.iter().map( - |instruction| { + column(recorder.instructions.iter().enumerate().map( + |(i, instruction)| { monospace(instruction.to_string()) .size(10) + .style(move |theme: &Theme| text::Style { + color: match &recorder.state { + State::Playing { + current, .. + } => { + if *current == i { + Some( + theme.palette().primary, + ) + } else if *current > i { + Some( + theme + .extended_palette() + .success + .strong + .color, + ) + } else { + None + } + } + _ => None, + }, + }) .into() }, )) @@ -491,19 +587,26 @@ where }) .width(Fill) .height(Fill) - .style(container::rounded_box) .padding(5); let controls = { row![ - button(icon::play().size(14).width(Fill).center()), - if recorder.is_recording { + button(icon::play().size(14).width(Fill).center()) + .on_press_maybe( + (!matches!(recorder.state, State::Recording) + && !recorder.instructions.is_empty()) + .then_some(Message::Play), + ), + if let State::Recording = &recorder.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(Message::Record) + .on_press_maybe( + matches!(recorder.state, State::Idle) + .then_some(Message::Record), + ) .style(button::danger) } ] @@ -544,23 +647,27 @@ where }; let content = row![if let Mode::Open { recorder } = &self.mode { - let is_recording = recorder.is_recording; - - let status = if is_recording { - monospace("Recording").style(|theme| text::Style { - color: Some(theme.palette().danger), - }) - } else { - monospace("Idle").style(|theme| text::Style { + let status = match &recorder.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), + }) + } + State::Playing { .. } => { + monospace("Playing").style(|theme| text::Style { + color: Some(theme.palette().primary), + }) + } }; let viewport = container( scrollable( - container(if recorder.is_recording { + container(if let State::Recording = &recorder.state { widget::recorder(view) .on_event(|event| { Event::Message(Message::Recorded(event)) @@ -577,14 +684,14 @@ where horizontal: scrollable::Scrollbar::default(), }), ) - .style(move |theme| { + .style(|theme| { let palette = theme.extended_palette(); container::Style { - border: border::width(2.0).color(if is_recording { - palette.danger.base.color - } else { - palette.background.strongest.color + border: border::width(2.0).color(match &recorder.state { + State::Idle => palette.background.strongest.color, + State::Recording => palette.danger.base.color, + State::Playing { .. } => palette.primary.base.color, }), ..container::Style::default() } @@ -654,6 +761,7 @@ where { Message(Message), Program(P::Message), + Emulator(emulator::Event

), Command(debug::Command), Discard, } @@ -666,6 +774,7 @@ where match self { Self::Message(message) => message.fmt(f), Self::Program(message) => message.fmt(f), + Self::Emulator(_) => f.write_str("Emulator"), Self::Command(command) => command.fmt(f), Self::Discard => f.write_str("Discard"), } @@ -682,6 +791,7 @@ where Self::Message(message) => Self::Message(message.clone()), Self::Program(message) => Self::Program(message.clone()), Self::Command(command) => Self::Command(*command), + Self::Emulator(_) => Self::Discard, // Time traveling an emulator?! Self::Discard => Self::Discard, } } diff --git a/futures/src/runtime.rs b/futures/src/runtime.rs index e25ba1d7..927b35db 100644 --- a/futures/src/runtime.rs +++ b/futures/src/runtime.rs @@ -2,7 +2,7 @@ use crate::subscription; use crate::{BoxStream, Executor, MaybeSend}; -use futures::{Sink, channel::mpsc}; +use futures::{Sink, SinkExt, channel::mpsc}; use std::marker::PhantomData; /// A batteries-included runtime of commands and subscriptions. @@ -79,6 +79,15 @@ where self.executor.spawn(future); } + /// Sends a message concurrently through the [`Runtime`]. + pub fn send(&mut self, message: Message) { + let mut sender = self.sender.clone(); + + self.executor.spawn(async move { + let _ = sender.send(message).await; + }); + } + /// Tracks a [`Subscription`] in the [`Runtime`]. /// /// It will spawn new streams or close old ones as necessary! See diff --git a/test/src/emulator.rs b/test/src/emulator.rs index 9b2d2549..c76301b5 100644 --- a/test/src/emulator.rs +++ b/test/src/emulator.rs @@ -27,6 +27,7 @@ pub struct Emulator { #[allow(missing_debug_implementations)] pub enum Event { Action(Action), + Ready, } impl Emulator

{ @@ -58,6 +59,9 @@ impl Emulator

{ runtime.run(stream.map(Event::Action).boxed()); } + // TODO: Async boot environments + runtime.send(Event::Ready); + Self { state, runtime, @@ -144,6 +148,8 @@ impl Emulator

{ for message in messages { self.update(program, message); } + + self.runtime.send(Event::Ready); } pub fn view( diff --git a/test/src/lib.rs b/test/src/lib.rs index 1aa69334..bdf06df1 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -96,6 +96,7 @@ pub mod simulator; mod error; +pub use emulator::Emulator; pub use error::Error; pub use instruction::Instruction; pub use selector::Selector;