Plug Emulator into devtools 🎉

This commit is contained in:
Héctor Ramón Jiménez 2025-05-31 05:50:25 +02:00
parent 16556b51bc
commit ed528c9c53
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
4 changed files with 158 additions and 32 deletions

View file

@ -25,6 +25,8 @@ use crate::futures::Subscription;
use crate::program::Program; use crate::program::Program;
use crate::runtime::Task; use crate::runtime::Task;
use crate::runtime::font; use crate::runtime::font;
use crate::test::Emulator;
use crate::test::emulator;
use crate::test::instruction; use crate::test::instruction;
use crate::time_machine::TimeMachine; use crate::time_machine::TimeMachine;
use crate::widget::{ use crate::widget::{
@ -126,7 +128,7 @@ where
{ {
state: P::State, state: P::State,
size: Size, size: Size,
mode: Mode, mode: Mode<P>,
show_notification: bool, show_notification: bool,
time_machine: TimeMachine<P>, time_machine: TimeMachine<P>,
} }
@ -145,17 +147,27 @@ pub enum Message {
Record, Record,
Stop, Stop,
Recorded(core::Event), Recorded(core::Event),
Play,
} }
enum Mode { enum Mode<P: Program> {
Hidden, Hidden,
Open { recorder: Recorder }, Open { recorder: Recorder<P> },
Setup(Setup), Setup(Setup),
} }
struct Recorder { struct Recorder<P: Program> {
instructions: Vec<test::Instruction>, instructions: Vec<test::Instruction>,
is_recording: bool, state: State<P>,
}
enum State<P: Program> {
Idle,
Recording,
Playing {
emulator: Emulator<P>,
current: usize,
},
} }
enum Setup { enum Setup {
@ -207,11 +219,16 @@ where
self.mode = Mode::Open { self.mode = Mode::Open {
recorder: Recorder { recorder: Recorder {
instructions: Vec::new(), 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; self.mode = Mode::Hidden;
} }
Mode::Setup(_) | Mode::Open { .. } => {} Mode::Setup(_) | Mode::Open { .. } => {}
@ -321,7 +338,7 @@ where
}; };
recorder.instructions.clear(); recorder.instructions.clear();
recorder.is_recording = true; recorder.state = State::Recording;
let (state, task) = program.boot(); let (state, task) = program.boot();
self.state = state; self.state = state;
@ -367,10 +384,27 @@ where
return Task::none(); return Task::none();
}; };
recorder.is_recording = false; recorder.state = State::Idle;
Task::none() 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) => { Event::Program(message) => {
self.time_machine.push(&message); self.time_machine.push(&message);
@ -402,6 +436,34 @@ where
Task::none() 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(), Event::Discard => Task::none(),
} }
} }
@ -414,7 +476,17 @@ where
let state = self.state(); let state = self.state();
let view = { 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 theme = program.theme(state, window);
let view: Element<'_, _, Theme, _> = themer(theme, view).into(); let view: Element<'_, _, Theme, _> = themer(theme, view).into();
@ -477,10 +549,34 @@ where
)) ))
} else { } else {
scrollable( scrollable(
column(recorder.instructions.iter().map( column(recorder.instructions.iter().enumerate().map(
|instruction| { |(i, instruction)| {
monospace(instruction.to_string()) monospace(instruction.to_string())
.size(10) .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() .into()
}, },
)) ))
@ -491,19 +587,26 @@ where
}) })
.width(Fill) .width(Fill)
.height(Fill) .height(Fill)
.style(container::rounded_box)
.padding(5); .padding(5);
let controls = { let controls = {
row![ row![
button(icon::play().size(14).width(Fill).center()), button(icon::play().size(14).width(Fill).center())
if recorder.is_recording { .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()) button(icon::stop().size(14).width(Fill).center())
.on_press(Message::Stop) .on_press(Message::Stop)
.style(button::success) .style(button::success)
} else { } else {
button(icon::record().size(14).width(Fill).center()) 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) .style(button::danger)
} }
] ]
@ -544,23 +647,27 @@ where
}; };
let content = row![if let Mode::Open { recorder } = &self.mode { let content = row![if let Mode::Open { recorder } = &self.mode {
let is_recording = recorder.is_recording; let status = match &recorder.state {
State::Idle => monospace("Idle").style(|theme| text::Style {
let status = if is_recording {
monospace("Recording").style(|theme| text::Style {
color: Some(theme.palette().danger),
})
} else {
monospace("Idle").style(|theme| text::Style {
color: Some( color: Some(
theme.extended_palette().background.strongest.color, 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( let viewport = container(
scrollable( scrollable(
container(if recorder.is_recording { container(if let State::Recording = &recorder.state {
widget::recorder(view) widget::recorder(view)
.on_event(|event| { .on_event(|event| {
Event::Message(Message::Recorded(event)) Event::Message(Message::Recorded(event))
@ -577,14 +684,14 @@ where
horizontal: scrollable::Scrollbar::default(), horizontal: scrollable::Scrollbar::default(),
}), }),
) )
.style(move |theme| { .style(|theme| {
let palette = theme.extended_palette(); let palette = theme.extended_palette();
container::Style { container::Style {
border: border::width(2.0).color(if is_recording { border: border::width(2.0).color(match &recorder.state {
palette.danger.base.color State::Idle => palette.background.strongest.color,
} else { State::Recording => palette.danger.base.color,
palette.background.strongest.color State::Playing { .. } => palette.primary.base.color,
}), }),
..container::Style::default() ..container::Style::default()
} }
@ -654,6 +761,7 @@ where
{ {
Message(Message), Message(Message),
Program(P::Message), Program(P::Message),
Emulator(emulator::Event<P>),
Command(debug::Command), Command(debug::Command),
Discard, Discard,
} }
@ -666,6 +774,7 @@ where
match self { match self {
Self::Message(message) => message.fmt(f), Self::Message(message) => message.fmt(f),
Self::Program(message) => message.fmt(f), Self::Program(message) => message.fmt(f),
Self::Emulator(_) => f.write_str("Emulator"),
Self::Command(command) => command.fmt(f), Self::Command(command) => command.fmt(f),
Self::Discard => f.write_str("Discard"), Self::Discard => f.write_str("Discard"),
} }
@ -682,6 +791,7 @@ where
Self::Message(message) => Self::Message(message.clone()), Self::Message(message) => Self::Message(message.clone()),
Self::Program(message) => Self::Program(message.clone()), Self::Program(message) => Self::Program(message.clone()),
Self::Command(command) => Self::Command(*command), Self::Command(command) => Self::Command(*command),
Self::Emulator(_) => Self::Discard, // Time traveling an emulator?!
Self::Discard => Self::Discard, Self::Discard => Self::Discard,
} }
} }

View file

@ -2,7 +2,7 @@
use crate::subscription; use crate::subscription;
use crate::{BoxStream, Executor, MaybeSend}; use crate::{BoxStream, Executor, MaybeSend};
use futures::{Sink, channel::mpsc}; use futures::{Sink, SinkExt, channel::mpsc};
use std::marker::PhantomData; use std::marker::PhantomData;
/// A batteries-included runtime of commands and subscriptions. /// A batteries-included runtime of commands and subscriptions.
@ -79,6 +79,15 @@ where
self.executor.spawn(future); 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`]. /// Tracks a [`Subscription`] in the [`Runtime`].
/// ///
/// It will spawn new streams or close old ones as necessary! See /// It will spawn new streams or close old ones as necessary! See

View file

@ -27,6 +27,7 @@ pub struct Emulator<P: Program> {
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
pub enum Event<P: Program> { pub enum Event<P: Program> {
Action(Action<P::Message>), Action(Action<P::Message>),
Ready,
} }
impl<P: Program + 'static> Emulator<P> { impl<P: Program + 'static> Emulator<P> {
@ -58,6 +59,9 @@ impl<P: Program + 'static> Emulator<P> {
runtime.run(stream.map(Event::Action).boxed()); runtime.run(stream.map(Event::Action).boxed());
} }
// TODO: Async boot environments
runtime.send(Event::Ready);
Self { Self {
state, state,
runtime, runtime,
@ -144,6 +148,8 @@ impl<P: Program + 'static> Emulator<P> {
for message in messages { for message in messages {
self.update(program, message); self.update(program, message);
} }
self.runtime.send(Event::Ready);
} }
pub fn view( pub fn view(

View file

@ -96,6 +96,7 @@ pub mod simulator;
mod error; mod error;
pub use emulator::Emulator;
pub use error::Error; pub use error::Error;
pub use instruction::Instruction; pub use instruction::Instruction;
pub use selector::Selector; pub use selector::Selector;