Draft test recorder structure in iced_devtools

This commit is contained in:
Héctor Ramón Jiménez 2025-05-28 19:58:43 +02:00
parent ca6d992d67
commit 327522eb99
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
16 changed files with 641 additions and 146 deletions

40
devtools/src/icon.rs Normal file
View file

@ -0,0 +1,40 @@
// Generated automatically by iced_fontello at build time.
// Do not edit manually. Source: ../fonts/iced_devtools-icons.toml
// 3139a163a989c992b8f038da359b59e9292fc49f031e760b61a8d76e2037aee2
use crate::core::Font;
use crate::program;
use crate::widget::{Text, text};
pub const FONT: &[u8] = include_bytes!("../fonts/iced_devtools-icons.ttf");
pub fn play<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer>
where
Theme: text::Catalog + 'a,
Renderer: program::Renderer,
{
icon("\u{25B6}")
}
pub fn record<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer>
where
Theme: text::Catalog + 'a,
Renderer: program::Renderer,
{
icon("\u{26AB}")
}
pub fn stop<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer>
where
Theme: text::Catalog + 'a,
Renderer: program::Renderer,
{
icon("\u{25A0}")
}
fn icon<'a, Theme, Renderer>(codepoint: &'a str) -> Text<'a, Theme, Renderer>
where
Theme: text::Catalog + 'a,
Renderer: program::Renderer,
{
text(codepoint).font(Font::with_name("iced_devtools-icons"))
}

View file

@ -8,21 +8,26 @@ use iced_widget::runtime::futures;
mod comet;
mod executor;
mod icon;
mod time_machine;
use crate::core::alignment::Horizontal::Right;
use crate::core::border;
use crate::core::keyboard;
use crate::core::theme::{self, Base, Theme};
use crate::core::time::seconds;
use crate::core::window;
use crate::core::{Alignment::Center, Color, Element, Length::Fill};
use crate::core::{
Alignment::Center, Color, Element, Font, Length::Fill, Size,
};
use crate::futures::Subscription;
use crate::program::Program;
use crate::runtime::Task;
use crate::runtime::font;
use crate::time_machine::TimeMachine;
use crate::widget::{
bottom_right, button, center, column, container, horizontal_space, opaque,
row, scrollable, stack, text, themer,
Text, bottom_right, button, center, column, container, horizontal_space,
opaque, row, scrollable, stack, text, text_input, themer,
};
use std::fmt;
@ -59,7 +64,11 @@ where
(
devtools,
Task::batch([boot.map(Event::Program), task.map(Event::Message)]),
Task::batch([
boot.map(Event::Program),
task.map(Event::Message),
font::load(icon::FONT).discard(),
]),
)
}
@ -110,6 +119,7 @@ where
P: Program,
{
state: P::State,
size: Size,
mode: Mode,
show_notification: bool,
time_machine: TimeMachine<P>,
@ -118,18 +128,28 @@ where
#[derive(Debug, Clone)]
pub enum Message {
HideNotification,
Toggle,
ToggleComet,
CometLaunched(comet::launch::Result),
InstallComet,
Installing(comet::install::Result),
CancelSetup,
ChangeWidth(String),
ChangeHeight(String),
Record,
}
enum Mode {
None,
Hidden,
Open { recorder: Recorder },
Setup(Setup),
}
enum Recorder {
Idle { events: Vec<core::Event> },
Recording { events: Vec<core::Event> },
}
enum Setup {
Idle { goal: Goal },
Running { logs: Vec<String> },
@ -148,7 +168,8 @@ where
(
Self {
state,
mode: Mode::None,
size: Size::new(512.0, 512.0),
mode: Mode::Hidden,
show_notification: true,
time_machine: TimeMachine::new(),
},
@ -172,10 +193,30 @@ where
Task::none()
}
Message::Toggle => {
match self.mode {
Mode::Hidden => {
self.mode = Mode::Open {
recorder: Recorder::Idle { events: Vec::new() },
};
}
Mode::Open {
recorder: Recorder::Idle { .. },
} => {
self.mode = Mode::Hidden;
}
Mode::Setup(_)
| Mode::Open {
recorder: Recorder::Recording { .. },
} => {}
}
Task::none()
}
Message::ToggleComet => {
if let Mode::Setup(setup) = &self.mode {
if matches!(setup, Setup::Idle { .. }) {
self.mode = Mode::None;
self.mode = Mode::Hidden;
}
Task::none()
@ -228,7 +269,7 @@ where
Task::none()
}
comet::install::Event::Finished => {
self.mode = Mode::None;
self.mode = Mode::Hidden;
comet::launch().discard()
}
}
@ -251,10 +292,34 @@ where
Task::none()
}
Message::CancelSetup => {
self.mode = Mode::None;
self.mode = Mode::Hidden;
Task::none()
}
Message::ChangeWidth(width) => {
if let Ok(width) = width.parse() {
self.size.width = width;
}
Task::none()
}
Message::ChangeHeight(height) => {
if let Ok(height) = height.parse() {
self.size.height = height;
}
Task::none()
}
Message::Record => {
let (state, task) = program.boot();
self.state = state;
self.mode = Mode::Open {
recorder: Recorder::Recording { events: Vec::new() },
};
task.map(Event::Program)
}
},
Event::Program(message) => {
self.time_machine.push(&message);
@ -299,6 +364,9 @@ where
let view = {
let view = program.view(state, window);
let theme = program.theme(state, window);
let view: Element<'_, _, Theme, _> = themer(theme, view).into();
if self.time_machine.is_rewinding() {
view.map(|_| Event::Discard)
@ -307,58 +375,177 @@ where
}
};
let theme = program.theme(state, window);
let theme = program
.theme(state, window)
.palette()
.map(|palette| Theme::custom("iced devtools", palette))
.unwrap_or_default();
let derive_theme = move || {
theme
.palette()
.map(|palette| Theme::custom("iced devtools", palette))
.unwrap_or_default()
};
let setup = if let Mode::Setup(setup) = &self.mode {
let stage: Element<'_, _, Theme, P::Renderer> = match setup {
Setup::Idle { goal } => self::setup(goal),
Setup::Running { logs } => installation(logs),
};
let mode = match &self.mode {
Mode::None => None,
Mode::Setup(setup) => {
let stage: Element<'_, _, Theme, P::Renderer> = match setup {
Setup::Idle { goal } => self::setup(goal),
Setup::Running { logs } => installation(logs),
};
let setup = center(
container(stage)
.padding(20)
.max_width(500)
.style(container::bordered_box),
)
.padding(10)
.style(|_theme| {
container::Style::default()
.background(Color::BLACK.scale_alpha(0.8))
});
let setup = center(
container(stage)
.padding(20)
.max_width(500)
.style(container::bordered_box),
)
.padding(10)
.style(|_theme| {
container::Style::default()
.background(Color::BLACK.scale_alpha(0.8))
});
Some(setup)
}
Some(setup)
} else {
None
}
.map(|mode| {
themer(derive_theme(), Element::from(mode).map(Event::Message))
});
.map(|mode| Element::from(mode).map(Event::Message));
let notification = self.show_notification.then(|| {
themer(
derive_theme(),
bottom_right(opaque(
container(text("Press F12 to open debug metrics"))
.padding(10)
.style(container::dark),
)),
)
bottom_right(opaque(
container(text("Press F12 to open developer tools"))
.padding(10)
.style(container::dark),
))
});
stack![view]
.height(Fill)
.push_maybe(mode.map(opaque))
.push_maybe(notification)
.into()
let sidebar = if let Mode::Open { recorder } = &self.mode {
let title = monospace("Developer Tools");
let recorder = {
let events = center(match recorder {
Recorder::Idle { events } if events.is_empty() => {
monospace("No events recorded yet!")
.size(14)
.width(Fill)
.center()
}
Recorder::Idle { events }
| Recorder::Recording { events } => {
monospace(format!("{} events recorded", events.len()))
}
})
.style(container::bordered_box);
let controls = {
row![
button(icon::play().size(14).width(Fill).center()),
match recorder {
Recorder::Idle { .. } => {
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)
};
column![events, controls].spacing(10).align_x(Center)
};
let viewport = row![
text_input("Width", &self.size.width.to_string())
.size(14)
.on_input(Message::ChangeWidth),
monospace("x"),
text_input("Height", &self.size.height.to_string())
.size(14)
.on_input(Message::ChangeHeight),
]
.spacing(10)
.align_y(Center);
let tools = column![
title,
labeled("Viewport", viewport),
labeled("Tester", recorder)
]
.spacing(10);
let sidebar = container(tools)
.padding(10)
.width(250)
.height(Fill)
.style(container::dark);
Some(Element::from(sidebar).map(Event::Message))
} else {
None
};
let content = row![if let Mode::Open { recorder } = &self.mode {
let is_recording = matches!(recorder, Recorder::Recording { .. });
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(
theme.extended_palette().background.strongest.color,
),
})
};
let viewport = container(
scrollable(
container(view)
.width(self.size.width)
.height(self.size.height),
)
.direction(scrollable::Direction::Both {
vertical: scrollable::Scrollbar::default(),
horizontal: scrollable::Scrollbar::default(),
}),
)
.style(move |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
}),
..container::Style::default()
}
})
.padding(10);
center(column![status, viewport].spacing(10).align_x(Right))
.padding(10)
.into()
} else {
view
}]
.push_maybe(sidebar);
themer(
theme,
stack![content]
.height(Fill)
.push_maybe(setup.map(opaque))
.push_maybe(notification),
)
.into()
}
fn subscription(&self, program: &P) -> Subscription<Event<P>> {
@ -369,6 +556,9 @@ where
let hotkeys =
futures::keyboard::on_key_press(|key, _modifiers| match key {
keyboard::Key::Named(keyboard::key::Named::F12) => {
Some(Message::Toggle)
}
keyboard::Key::Named(keyboard::key::Named::F11) => {
Some(Message::ToggleComet)
}
_ => None,
@ -438,7 +628,7 @@ where
fn setup<Renderer>(goal: &Goal) -> Element<'_, Message, Theme, Renderer>
where
Renderer: core::text::Renderer + 'static,
Renderer: program::Renderer + 'static,
{
let controls = row![
button(text("Cancel").center().width(Fill))
@ -460,14 +650,13 @@ where
];
let command = container(
text!(
monospace(format!(
"cargo install --locked \\
--git https://github.com/iced-rs/comet.git \\
--rev {}",
comet::COMPATIBLE_REVISION
)
.size(14)
.font(Renderer::MONOSPACE_FONT),
))
.size(14),
)
.width(Fill)
.padding(5)
@ -528,15 +717,15 @@ fn installation<'a, Renderer>(
logs: &'a [String],
) -> Element<'a, Message, Theme, Renderer>
where
Renderer: core::text::Renderer + 'a,
Renderer: program::Renderer + 'a,
{
column![
text("Installing comet...").size(20),
container(
scrollable(
column(logs.iter().map(|log| {
text(log).size(12).font(Renderer::MONOSPACE_FONT).into()
}),)
column(
logs.iter().map(|log| { monospace(log).size(12).into() }),
)
.spacing(3),
)
.spacing(10)
@ -555,9 +744,9 @@ fn inline_code<'a, Renderer>(
code: impl text::IntoFragment<'a>,
) -> Element<'a, Message, Theme, Renderer>
where
Renderer: core::text::Renderer + 'a,
Renderer: program::Renderer + 'a,
{
container(text(code).font(Renderer::MONOSPACE_FONT).size(12))
container(monospace(code).size(12))
.style(|_theme| {
container::Style::default()
.background(Color::BLACK)
@ -566,3 +755,25 @@ where
.padding([2, 4])
.into()
}
fn monospace<'a, Renderer>(
fragment: impl text::IntoFragment<'a>,
) -> Text<'a, Theme, Renderer>
where
Renderer: program::Renderer + 'a,
{
text(fragment).font(Font::MONOSPACE)
}
fn labeled<'a, Message, Renderer>(
fragment: impl text::IntoFragment<'a>,
content: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Renderer: program::Renderer + 'a,
{
column![monospace(fragment).size(14), content.into()]
.spacing(5)
.into()
}