Implement instruction editor for tester

This commit is contained in:
Héctor Ramón Jiménez 2025-06-05 15:11:25 +02:00
parent 76213a55f5
commit f878b59977
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
4 changed files with 134 additions and 10 deletions

View file

@ -10,3 +10,6 @@ check = "entypo-check"
cancel = "entypo-cancel" cancel = "entypo-cancel"
folder = "entypo-folder" folder = "entypo-folder"
floppy = "entypo-floppy" floppy = "entypo-floppy"
pencil = "entypo-pencil"
mouse_pointer = "fontawesome-mouse-pointer"
keyboard = "entypo-keyboard"

View file

@ -37,6 +37,14 @@ where
icon("\u{1F4C1}") icon("\u{1F4C1}")
} }
pub fn keyboard<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer>
where
Theme: text::Catalog + 'a,
Renderer: program::Renderer,
{
icon("\u{2328}")
}
pub fn lightbulb<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer> pub fn lightbulb<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer>
where where
Theme: text::Catalog + 'a, Theme: text::Catalog + 'a,
@ -45,6 +53,14 @@ where
icon("\u{F0EB}") icon("\u{F0EB}")
} }
pub fn mouse_pointer<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer>
where
Theme: text::Catalog + 'a,
Renderer: program::Renderer,
{
icon("\u{F245}")
}
pub fn pause<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer> pub fn pause<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer>
where where
Theme: text::Catalog + 'a, Theme: text::Catalog + 'a,
@ -53,6 +69,14 @@ where
icon("\u{2389}") icon("\u{2389}")
} }
pub fn pencil<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer>
where
Theme: text::Catalog + 'a,
Renderer: program::Renderer,
{
icon("\u{270E}")
}
pub fn play<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer> pub fn play<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer>
where where
Theme: text::Catalog + 'a, Theme: text::Catalog + 'a,

View file

@ -8,7 +8,7 @@ use crate::core::Length::Fill;
use crate::core::alignment::Horizontal::Right; use crate::core::alignment::Horizontal::Right;
use crate::core::border; use crate::core::border;
use crate::core::window; use crate::core::window;
use crate::core::{Element, Event, Size, Theme}; use crate::core::{Element, Event, Font, Size, Theme};
use crate::executor; use crate::executor;
use crate::futures::Subscription; use crate::futures::Subscription;
use crate::futures::futures::channel::mpsc; use crate::futures::futures::channel::mpsc;
@ -19,8 +19,8 @@ use crate::test::emulator;
use crate::test::instruction; use crate::test::instruction;
use crate::test::{Emulator, Instruction}; use crate::test::{Emulator, Instruction};
use crate::widget::{ use crate::widget::{
button, center, column, combo_box, container, monospace, pick_list, row, button, center, column, combo_box, container, horizontal_space, monospace,
scrollable, text, text_input, themer, pick_list, row, scrollable, text, text_editor, text_input, themer,
}; };
pub struct Tester<P: Program> { pub struct Tester<P: Program> {
@ -30,6 +30,7 @@ pub struct Tester<P: Program> {
preset: Option<String>, preset: Option<String>,
instructions: Vec<Instruction>, instructions: Vec<Instruction>,
state: State<P>, state: State<P>,
edit: Option<text_editor::Content<P::Renderer>>,
} }
enum State<P: Program> { enum State<P: Program> {
@ -64,6 +65,9 @@ pub enum Message {
Import, Import,
Export, Export,
Imported(Option<String>), Imported(Option<String>),
Edit,
Edited(text_editor::Action),
Confirm,
} }
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
@ -90,6 +94,7 @@ impl<P: Program + 'static> Tester<P> {
preset: None, preset: None,
instructions: Vec::new(), instructions: Vec::new(),
state: State::Idle, state: State::Idle,
edit: None,
} }
} }
@ -126,6 +131,7 @@ impl<P: Program + 'static> Tester<P> {
Task::none() Task::none()
} }
Message::Record => { Message::Record => {
self.edit = None;
self.instructions.clear(); self.instructions.clear();
let (state, task) = if let Some(preset) = self.preset(program) { let (state, task) = if let Some(preset) = self.preset(program) {
@ -150,6 +156,8 @@ impl<P: Program + 'static> Tester<P> {
Task::none() Task::none()
} }
Message::Play => { Message::Play => {
self.confirm();
let (sender, receiver) = mpsc::channel(1); let (sender, receiver) = mpsc::channel(1);
let emulator = Emulator::with_preset( let emulator = Emulator::with_preset(
@ -189,6 +197,8 @@ impl<P: Program + 'static> Tester<P> {
use std::fs; use std::fs;
use std::thread; use std::thread;
self.confirm();
let test: Vec<_> = self let test: Vec<_> = self
.instructions .instructions
.iter() .iter()
@ -221,17 +231,60 @@ impl<P: Program + 'static> Tester<P> {
match instructions { match instructions {
Ok(instructions) => { Ok(instructions) => {
self.instructions = instructions; self.instructions = instructions;
self.edit = None;
} }
Err(error) => { Err(error) => {
log::error!("{error}"); log::error!("{error}");
} }
} }
Task::none()
}
Message::Edit => {
if self.is_busy() {
return Task::none();
}
self.edit = Some(text_editor::Content::with_text(
&self
.instructions
.iter()
.map(Instruction::to_string)
.collect::<Vec<_>>()
.join("\n"),
));
Task::none()
}
Message::Edited(action) => {
if let Some(edit) = &mut self.edit {
edit.perform(action);
}
Task::none()
}
Message::Confirm => {
self.confirm();
Task::none() Task::none()
} }
} }
} }
fn confirm(&mut self) {
let Some(edit) = &mut self.edit else {
return;
};
self.instructions = edit
.lines()
.filter(|line| !line.text.trim().is_empty())
.filter_map(|line| Instruction::parse(&line.text).ok())
.collect();
self.edit = None;
}
fn preset<'a>( fn preset<'a>(
&self, &self,
program: &'a P, program: &'a P,
@ -472,7 +525,14 @@ impl<P: Program + 'static> Tester<P> {
.width(Fill); .width(Fill);
let player = { let player = {
let instructions = container(if self.instructions.is_empty() { let instructions = if let Some(edit) = &self.edit {
text_editor(edit)
.size(12)
.height(Fill)
.font(Font::MONOSPACE)
.on_action(Message::Edited)
.into()
} else if self.instructions.is_empty() {
Element::from(center( Element::from(center(
monospace("No instructions recorded yet!") monospace("No instructions recorded yet!")
.size(14) .size(14)
@ -497,7 +557,6 @@ impl<P: Program + 'static> Tester<P> {
Outcome::Running => { Outcome::Running => {
theme.palette().primary theme.palette().primary
} }
Outcome::Failed => { Outcome::Failed => {
theme theme
.extended_palette() .extended_palette()
@ -533,12 +592,11 @@ impl<P: Program + 'static> Tester<P> {
)) ))
.spacing(5), .spacing(5),
) )
.width(Fill)
.height(Fill)
.spacing(5) .spacing(5)
.into() .into()
}) };
.width(Fill)
.height(Fill)
.padding(5);
let control = |icon: text::Text<'static, _, _>| { let control = |icon: text::Text<'static, _, _>| {
button(icon.size(14).width(Fill).height(Fill).center()) button(icon.size(14).width(Fill).height(Fill).center())
@ -580,11 +638,27 @@ impl<P: Program + 'static> Tester<P> {
column![instructions, controls].spacing(10).align_x(Center) column![instructions, controls].spacing(10).align_x(Center)
}; };
let edit = if self.is_busy() {
Element::from(horizontal_space())
} else if self.edit.is_none() {
button(icon::pencil().size(14))
.padding(0)
.on_press(Message::Edit)
.style(button::text)
.into()
} else {
button(icon::check().size(14))
.padding(0)
.on_press(Message::Confirm)
.style(button::text)
.into()
};
column![ column![
labeled("Viewport", viewport), labeled("Viewport", viewport),
labeled("Mode", mode), labeled("Mode", mode),
labeled("Preset", preset), labeled("Preset", preset),
labeled("Instructions", player) labeled_with("Instructions", edit, player)
] ]
.spacing(10) .spacing(10)
.into() .into()
@ -603,3 +677,26 @@ where
.spacing(5) .spacing(5)
.into() .into()
} }
fn labeled_with<'a, Message, Renderer>(
fragment: impl text::IntoFragment<'a>,
control: impl Into<Element<'a, Message, Theme, Renderer>>,
content: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Renderer: program::Renderer + 'a,
{
column![
row![
monospace(fragment).size(14),
horizontal_space(),
control.into()
]
.spacing(5)
.align_y(Center),
content.into()
]
.spacing(5)
.into()
}