Draft Instruction DSL in iced_test

This commit is contained in:
Héctor Ramón Jiménez 2025-05-29 16:34:44 +02:00
parent 327522eb99
commit 921467b5be
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
12 changed files with 1246 additions and 631 deletions

View file

@ -1,7 +1,7 @@
#![allow(missing_docs)]
use iced_debug as debug;
use iced_program as program;
use iced_widget as widget;
use iced_test as test;
use iced_widget::core;
use iced_widget::runtime;
use iced_widget::runtime::futures;
@ -10,6 +10,7 @@ mod comet;
mod executor;
mod icon;
mod time_machine;
mod widget;
use crate::core::alignment::Horizontal::Right;
use crate::core::border;
@ -24,6 +25,7 @@ use crate::futures::Subscription;
use crate::program::Program;
use crate::runtime::Task;
use crate::runtime::font;
use crate::test::instruction;
use crate::time_machine::TimeMachine;
use crate::widget::{
Text, bottom_right, button, center, column, container, horizontal_space,
@ -137,6 +139,8 @@ pub enum Message {
ChangeWidth(String),
ChangeHeight(String),
Record,
Stop,
Recorded(core::Event),
}
enum Mode {
@ -145,9 +149,9 @@ enum Mode {
Setup(Setup),
}
enum Recorder {
Idle { events: Vec<core::Event> },
Recording { events: Vec<core::Event> },
struct Recorder {
instructions: Vec<test::Instruction>,
is_recording: bool,
}
enum Setup {
@ -194,21 +198,19 @@ where
Task::none()
}
Message::Toggle => {
match self.mode {
match &self.mode {
Mode::Hidden => {
self.mode = Mode::Open {
recorder: Recorder::Idle { events: Vec::new() },
recorder: Recorder {
instructions: Vec::new(),
is_recording: false,
},
};
}
Mode::Open {
recorder: Recorder::Idle { .. },
} => {
Mode::Open { recorder } if !recorder.is_recording => {
self.mode = Mode::Hidden;
}
Mode::Setup(_)
| Mode::Open {
recorder: Recorder::Recording { .. },
} => {}
Mode::Setup(_) | Mode::Open { .. } => {}
}
Task::none()
@ -256,7 +258,6 @@ where
.map(Message::Installing)
.map(Event::Message)
}
Message::Installing(Ok(installation)) => {
let Mode::Setup(Setup::Running { logs }) = &mut self.mode
else {
@ -311,15 +312,61 @@ where
Task::none()
}
Message::Record => {
let (state, task) = program.boot();
self.state = state;
self.mode = Mode::Open {
recorder: Recorder::Recording { events: Vec::new() },
let Mode::Open { recorder } = &mut self.mode else {
return Task::none();
};
recorder.instructions.clear();
recorder.is_recording = true;
let (state, task) = program.boot();
self.state = state;
task.map(Event::Program)
}
Message::Recorded(event) => {
let Mode::Open { recorder } = &mut self.mode else {
return Task::none();
};
let Some(interaction) =
instruction::Interaction::from_event(event)
else {
return Task::none();
};
if let Some(test::Instruction::Interact(last_interaction)) =
recorder.instructions.pop()
{
let (last_interaction, new_interaction) =
last_interaction.merge(interaction);
recorder.instructions.push(
test::Instruction::Interact(last_interaction),
);
if let Some(new_interaction) = new_interaction {
recorder.instructions.push(
test::Instruction::Interact(new_interaction),
);
}
} else {
recorder
.instructions
.push(test::Instruction::Interact(interaction));
}
Task::none()
}
Message::Stop => {
let Mode::Open { recorder } = &mut self.mode else {
return Task::none();
};
recorder.is_recording = false;
Task::none()
}
},
Event::Program(message) => {
self.time_machine.push(&message);
@ -417,41 +464,43 @@ where
let title = monospace("Developer Tools");
let recorder = {
let events = center(match recorder {
Recorder::Idle { events } if events.is_empty() => {
monospace("No events recorded yet!")
let events = container(if recorder.instructions.is_empty() {
Element::from(center(
monospace("No instructions recorded yet!")
.size(14)
.width(Fill)
.center()
}
Recorder::Idle { events }
| Recorder::Recording { events } => {
monospace(format!("{} events recorded", events.len()))
}
.center(),
))
} else {
scrollable(
column(recorder.instructions.iter().map(
|instruction| {
monospace(instruction.to_string())
.size(10)
.into()
},
))
.spacing(5),
)
.spacing(5)
.into()
})
.style(container::bordered_box);
.width(Fill)
.height(Fill)
.style(container::rounded_box)
.padding(5);
let controls = {
row![
button(icon::play().size(14).width(Fill).center()),
match recorder {
Recorder::Idle { .. } => {
button(
icon::record()
.size(14)
.width(Fill)
.center(),
)
if recorder.is_recording {
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)
.style(button::danger)
}
Recorder::Recording { .. } => {
button(
icon::stop().size(14).width(Fill).center(),
)
.on_press(Message::Record)
.style(button::success)
}
}
]
.spacing(10)
@ -491,7 +540,7 @@ where
};
let content = row![if let Mode::Open { recorder } = &self.mode {
let is_recording = matches!(recorder, Recorder::Recording { .. });
let is_recording = recorder.is_recording;
let status = if is_recording {
monospace("Recording").style(|theme| text::Style {
@ -507,9 +556,17 @@ where
let viewport = container(
scrollable(
container(view)
.width(self.size.width)
.height(self.size.height),
container(if recorder.is_recording {
widget::recorder(view)
.on_event(|event| {
Event::Message(Message::Recorded(event))
})
.into()
} else {
view
})
.width(self.size.width)
.height(self.size.height),
)
.direction(scrollable::Direction::Both {
vertical: scrollable::Scrollbar::default(),
@ -673,7 +730,7 @@ where
your iced applications.",
column![
"Do you wish to install it with the \
following command?",
following command?",
command
]
.spacing(10),

12
devtools/src/widget.rs Normal file
View file

@ -0,0 +1,12 @@
mod recorder;
pub use iced_widget::*;
pub use recorder::Recorder;
use crate::core::Element;
pub fn recorder<'a, Message, Theme, Renderer>(
content: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Recorder<'a, Message, Theme, Renderer> {
Recorder::new(content)
}

View file

@ -0,0 +1,178 @@
use crate::core::layout;
use crate::core::mouse;
use crate::core::renderer;
use crate::core::widget;
use crate::core::widget::tree;
use crate::core::{
self, Clipboard, Element, Event, Layout, Length, Point, Rectangle, Shell,
Size, Widget,
};
#[allow(missing_debug_implementations)]
pub struct Recorder<'a, Message, Theme, Renderer> {
content: Element<'a, Message, Theme, Renderer>,
on_event: Option<Box<dyn Fn(Event) -> Message + 'a>>,
}
impl<'a, Message, Theme, Renderer> Recorder<'a, Message, Theme, Renderer> {
pub fn new(
content: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Self {
Self {
content: content.into(),
on_event: None,
}
}
pub fn on_event(
mut self,
on_event: impl Fn(Event) -> Message + 'a,
) -> Self {
self.on_event = Some(Box::new(on_event));
self
}
}
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Recorder<'_, Message, Theme, Renderer>
where
Renderer: core::Renderer,
{
fn update(
&mut self,
state: &mut widget::Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
if shell.is_event_captured() {
return;
}
self.content.as_widget_mut().update(
state, event, layout, cursor, renderer, clipboard, shell, viewport,
);
if let Some(on_event) = &self.on_event {
match event {
Event::Mouse(event) => {
if !cursor.is_over(layout.bounds()) {
return;
}
match event {
mouse::Event::ButtonPressed(_)
| mouse::Event::ButtonReleased(_)
| mouse::Event::WheelScrolled { .. } => {
shell
.publish(on_event(Event::Mouse(event.clone())));
}
mouse::Event::CursorMoved { position } => {
shell.publish(on_event(Event::Mouse(
mouse::Event::CursorMoved {
position: *position
- (layout.bounds().position()
- Point::ORIGIN),
},
)));
}
_ => {}
}
}
Event::Keyboard(event) => {
shell.publish(on_event(Event::Keyboard(event.clone())));
}
_ => {}
}
}
}
fn tag(&self) -> tree::Tag {
self.content.as_widget().tag()
}
fn state(&self) -> tree::State {
self.content.as_widget().state()
}
fn children(&self) -> Vec<widget::Tree> {
self.content.as_widget().children()
}
fn diff(&self, tree: &mut tree::Tree) {
self.content.as_widget().diff(tree);
}
fn size(&self) -> Size<Length> {
self.content.as_widget().size()
}
fn size_hint(&self) -> Size<Length> {
self.content.as_widget().size_hint()
}
fn layout(
&self,
tree: &mut widget::Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
self.content.as_widget().layout(tree, renderer, limits)
}
fn draw(
&self,
tree: &widget::Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
self.content
.as_widget()
.draw(tree, renderer, theme, style, layout, cursor, viewport);
}
fn mouse_interaction(
&self,
state: &widget::Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.content
.as_widget()
.mouse_interaction(state, layout, cursor, viewport, renderer)
}
fn operate(
&self,
state: &mut widget::Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn widget::Operation,
) {
self.content
.as_widget()
.operate(state, layout, renderer, operation);
}
}
impl<'a, Message, Theme, Renderer> From<Recorder<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Theme: 'a,
Renderer: core::Renderer + 'a,
{
fn from(recorder: Recorder<'a, Message, Theme, Renderer>) -> Self {
Element::new(recorder)
}
}