Draft test recorder structure in iced_devtools
This commit is contained in:
parent
ca6d992d67
commit
327522eb99
16 changed files with 641 additions and 146 deletions
40
devtools/src/icon.rs
Normal file
40
devtools/src/icon.rs
Normal 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"))
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue