2025-04-05 19:27:15 +02:00
|
|
|
#![allow(missing_docs)]
|
2025-04-05 20:08:54 +02:00
|
|
|
use iced_debug as debug;
|
2025-04-05 19:27:15 +02:00
|
|
|
use iced_program as program;
|
2025-04-05 20:08:54 +02:00
|
|
|
use iced_widget as widget;
|
2025-04-05 19:27:15 +02:00
|
|
|
use iced_widget::core;
|
|
|
|
|
use iced_widget::runtime;
|
2025-04-05 20:08:54 +02:00
|
|
|
use iced_widget::runtime::futures;
|
2025-04-05 19:27:15 +02:00
|
|
|
|
2025-04-28 09:48:55 +02:00
|
|
|
mod comet;
|
2025-04-06 17:21:20 +02:00
|
|
|
mod executor;
|
2025-05-28 19:58:43 +02:00
|
|
|
mod icon;
|
2025-04-20 22:11:24 +02:00
|
|
|
mod time_machine;
|
2025-04-06 17:21:20 +02:00
|
|
|
|
2025-05-28 19:58:43 +02:00
|
|
|
use crate::core::alignment::Horizontal::Right;
|
2025-04-28 22:31:13 +02:00
|
|
|
use crate::core::border;
|
2025-04-05 20:08:54 +02:00
|
|
|
use crate::core::keyboard;
|
|
|
|
|
use crate::core::theme::{self, Base, Theme};
|
|
|
|
|
use crate::core::time::seconds;
|
2025-04-05 19:27:15 +02:00
|
|
|
use crate::core::window;
|
2025-05-28 19:58:43 +02:00
|
|
|
use crate::core::{
|
|
|
|
|
Alignment::Center, Color, Element, Font, Length::Fill, Size,
|
|
|
|
|
};
|
2025-04-05 19:27:15 +02:00
|
|
|
use crate::futures::Subscription;
|
|
|
|
|
use crate::program::Program;
|
|
|
|
|
use crate::runtime::Task;
|
2025-05-28 19:58:43 +02:00
|
|
|
use crate::runtime::font;
|
2025-04-20 22:11:24 +02:00
|
|
|
use crate::time_machine::TimeMachine;
|
2025-04-06 17:21:20 +02:00
|
|
|
use crate::widget::{
|
2025-05-28 19:58:43 +02:00
|
|
|
Text, bottom_right, button, center, column, container, horizontal_space,
|
|
|
|
|
opaque, row, scrollable, stack, text, text_input, themer,
|
2025-04-06 17:21:20 +02:00
|
|
|
};
|
2025-04-05 19:27:15 +02:00
|
|
|
|
|
|
|
|
use std::fmt;
|
2025-04-05 20:14:51 +02:00
|
|
|
use std::thread;
|
2025-04-05 19:27:15 +02:00
|
|
|
|
2025-05-04 22:39:23 +02:00
|
|
|
pub fn attach<P: Program + 'static>(program: P) -> Attach<P> {
|
|
|
|
|
Attach { program }
|
|
|
|
|
}
|
2025-04-05 19:27:15 +02:00
|
|
|
|
2025-05-04 22:39:23 +02:00
|
|
|
/// A [`Program`] with some devtools attached to it.
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct Attach<P> {
|
|
|
|
|
/// The original [`Program`] managed by these devtools.
|
|
|
|
|
pub program: P,
|
|
|
|
|
}
|
2025-04-05 19:27:15 +02:00
|
|
|
|
2025-05-04 22:39:23 +02:00
|
|
|
impl<P> Program for Attach<P>
|
|
|
|
|
where
|
|
|
|
|
P: Program + 'static,
|
|
|
|
|
{
|
|
|
|
|
type State = DevTools<P>;
|
|
|
|
|
type Message = Event<P>;
|
|
|
|
|
type Theme = P::Theme;
|
|
|
|
|
type Renderer = P::Renderer;
|
|
|
|
|
type Executor = P::Executor;
|
|
|
|
|
|
|
|
|
|
fn name() -> &'static str {
|
|
|
|
|
P::name()
|
|
|
|
|
}
|
2025-04-05 19:27:15 +02:00
|
|
|
|
2025-05-04 22:39:23 +02:00
|
|
|
fn boot(&self) -> (Self::State, Task<Self::Message>) {
|
|
|
|
|
let (state, boot) = self.program.boot();
|
|
|
|
|
let (devtools, task) = DevTools::new(state);
|
2025-04-05 19:27:15 +02:00
|
|
|
|
2025-05-04 22:39:23 +02:00
|
|
|
(
|
|
|
|
|
devtools,
|
2025-05-28 19:58:43 +02:00
|
|
|
Task::batch([
|
|
|
|
|
boot.map(Event::Program),
|
|
|
|
|
task.map(Event::Message),
|
|
|
|
|
font::load(icon::FONT).discard(),
|
|
|
|
|
]),
|
2025-05-04 22:39:23 +02:00
|
|
|
)
|
|
|
|
|
}
|
2025-04-05 19:27:15 +02:00
|
|
|
|
2025-05-04 22:39:23 +02:00
|
|
|
fn update(
|
|
|
|
|
&self,
|
|
|
|
|
state: &mut Self::State,
|
|
|
|
|
message: Self::Message,
|
|
|
|
|
) -> Task<Self::Message> {
|
|
|
|
|
state.update(&self.program, message)
|
|
|
|
|
}
|
2025-04-05 19:27:15 +02:00
|
|
|
|
2025-05-04 22:39:23 +02:00
|
|
|
fn view<'a>(
|
|
|
|
|
&self,
|
|
|
|
|
state: &'a Self::State,
|
|
|
|
|
window: window::Id,
|
|
|
|
|
) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> {
|
|
|
|
|
state.view(&self.program, window)
|
|
|
|
|
}
|
2025-04-05 19:27:15 +02:00
|
|
|
|
2025-05-04 22:39:23 +02:00
|
|
|
fn title(&self, state: &Self::State, window: window::Id) -> String {
|
|
|
|
|
state.title(&self.program, window)
|
|
|
|
|
}
|
2025-04-05 19:27:15 +02:00
|
|
|
|
2025-05-04 22:39:23 +02:00
|
|
|
fn subscription(
|
|
|
|
|
&self,
|
|
|
|
|
state: &Self::State,
|
|
|
|
|
) -> runtime::futures::Subscription<Self::Message> {
|
|
|
|
|
state.subscription(&self.program)
|
|
|
|
|
}
|
2025-04-05 19:27:15 +02:00
|
|
|
|
2025-05-04 22:39:23 +02:00
|
|
|
fn theme(&self, state: &Self::State, window: window::Id) -> Self::Theme {
|
|
|
|
|
state.theme(&self.program, window)
|
|
|
|
|
}
|
2025-04-05 19:27:15 +02:00
|
|
|
|
2025-05-04 22:39:23 +02:00
|
|
|
fn style(&self, state: &Self::State, theme: &Self::Theme) -> theme::Style {
|
|
|
|
|
state.style(&self.program, theme)
|
2025-04-05 19:27:15 +02:00
|
|
|
}
|
|
|
|
|
|
2025-05-04 22:39:23 +02:00
|
|
|
fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 {
|
|
|
|
|
state.scale_factor(&self.program, window)
|
|
|
|
|
}
|
2025-04-05 19:27:15 +02:00
|
|
|
}
|
|
|
|
|
|
2025-05-04 22:39:23 +02:00
|
|
|
/// The state of the devtools.
|
|
|
|
|
#[allow(missing_debug_implementations)]
|
|
|
|
|
pub struct DevTools<P>
|
2025-04-05 19:27:15 +02:00
|
|
|
where
|
|
|
|
|
P: Program,
|
|
|
|
|
{
|
|
|
|
|
state: P::State,
|
2025-05-28 19:58:43 +02:00
|
|
|
size: Size,
|
2025-04-06 17:21:20 +02:00
|
|
|
mode: Mode,
|
2025-04-05 20:08:54 +02:00
|
|
|
show_notification: bool,
|
2025-04-20 22:11:24 +02:00
|
|
|
time_machine: TimeMachine<P>,
|
2025-04-05 19:27:15 +02:00
|
|
|
}
|
|
|
|
|
|
2025-04-06 17:21:20 +02:00
|
|
|
#[derive(Debug, Clone)]
|
2025-05-04 22:39:23 +02:00
|
|
|
pub enum Message {
|
2025-04-06 17:21:20 +02:00
|
|
|
HideNotification,
|
2025-05-28 19:58:43 +02:00
|
|
|
Toggle,
|
2025-04-06 17:21:20 +02:00
|
|
|
ToggleComet,
|
2025-04-28 22:31:13 +02:00
|
|
|
CometLaunched(comet::launch::Result),
|
2025-04-06 17:21:20 +02:00
|
|
|
InstallComet,
|
2025-04-28 22:31:13 +02:00
|
|
|
Installing(comet::install::Result),
|
2025-04-06 17:21:20 +02:00
|
|
|
CancelSetup,
|
2025-05-28 19:58:43 +02:00
|
|
|
ChangeWidth(String),
|
|
|
|
|
ChangeHeight(String),
|
|
|
|
|
Record,
|
2025-04-06 17:21:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
enum Mode {
|
2025-05-28 19:58:43 +02:00
|
|
|
Hidden,
|
|
|
|
|
Open { recorder: Recorder },
|
2025-04-06 17:21:20 +02:00
|
|
|
Setup(Setup),
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-28 19:58:43 +02:00
|
|
|
enum Recorder {
|
|
|
|
|
Idle { events: Vec<core::Event> },
|
|
|
|
|
Recording { events: Vec<core::Event> },
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-06 17:21:20 +02:00
|
|
|
enum Setup {
|
2025-04-28 09:48:55 +02:00
|
|
|
Idle { goal: Goal },
|
2025-04-06 17:21:20 +02:00
|
|
|
Running { logs: Vec<String> },
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-28 09:48:55 +02:00
|
|
|
enum Goal {
|
|
|
|
|
Installation,
|
|
|
|
|
Update { revision: Option<String> },
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-05 19:27:15 +02:00
|
|
|
impl<P> DevTools<P>
|
|
|
|
|
where
|
|
|
|
|
P: Program + 'static,
|
|
|
|
|
{
|
2025-04-20 22:11:24 +02:00
|
|
|
fn new(state: P::State) -> (Self, Task<Message>) {
|
2025-04-05 20:08:54 +02:00
|
|
|
(
|
|
|
|
|
Self {
|
|
|
|
|
state,
|
2025-05-28 19:58:43 +02:00
|
|
|
size: Size::new(512.0, 512.0),
|
|
|
|
|
mode: Mode::Hidden,
|
2025-04-05 20:08:54 +02:00
|
|
|
show_notification: true,
|
2025-04-20 22:11:24 +02:00
|
|
|
time_machine: TimeMachine::new(),
|
2025-04-05 20:08:54 +02:00
|
|
|
},
|
2025-04-06 17:21:20 +02:00
|
|
|
executor::spawn_blocking(|mut sender| {
|
|
|
|
|
thread::sleep(seconds(2));
|
|
|
|
|
let _ = sender.try_send(());
|
|
|
|
|
})
|
|
|
|
|
.map(|_| Message::HideNotification),
|
2025-04-05 20:08:54 +02:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-20 22:11:24 +02:00
|
|
|
fn title(&self, program: &P, window: window::Id) -> String {
|
2025-04-05 19:27:15 +02:00
|
|
|
program.title(&self.state, window)
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-20 22:11:24 +02:00
|
|
|
fn update(&mut self, program: &P, event: Event<P>) -> Task<Event<P>> {
|
2025-04-06 17:21:20 +02:00
|
|
|
match event {
|
|
|
|
|
Event::Message(message) => match message {
|
|
|
|
|
Message::HideNotification => {
|
|
|
|
|
self.show_notification = false;
|
2025-04-05 20:08:54 +02:00
|
|
|
|
2025-04-06 17:21:20 +02:00
|
|
|
Task::none()
|
|
|
|
|
}
|
2025-05-28 19:58:43 +02:00
|
|
|
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()
|
|
|
|
|
}
|
2025-04-06 17:21:20 +02:00
|
|
|
Message::ToggleComet => {
|
|
|
|
|
if let Mode::Setup(setup) = &self.mode {
|
2025-04-28 09:48:55 +02:00
|
|
|
if matches!(setup, Setup::Idle { .. }) {
|
2025-05-28 19:58:43 +02:00
|
|
|
self.mode = Mode::Hidden;
|
2025-04-06 17:21:20 +02:00
|
|
|
}
|
2025-04-28 09:48:55 +02:00
|
|
|
|
|
|
|
|
Task::none()
|
|
|
|
|
} else if debug::quit() {
|
|
|
|
|
Task::none()
|
|
|
|
|
} else {
|
|
|
|
|
comet::launch()
|
|
|
|
|
.map(Message::CometLaunched)
|
|
|
|
|
.map(Event::Message)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Message::CometLaunched(Ok(())) => Task::none(),
|
|
|
|
|
Message::CometLaunched(Err(error)) => {
|
|
|
|
|
match error {
|
2025-04-28 22:31:13 +02:00
|
|
|
comet::launch::Error::NotFound => {
|
2025-04-28 09:48:55 +02:00
|
|
|
self.mode = Mode::Setup(Setup::Idle {
|
|
|
|
|
goal: Goal::Installation,
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-04-28 22:31:13 +02:00
|
|
|
comet::launch::Error::Outdated { revision } => {
|
2025-04-28 09:48:55 +02:00
|
|
|
self.mode = Mode::Setup(Setup::Idle {
|
|
|
|
|
goal: Goal::Update { revision },
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-04-28 22:31:13 +02:00
|
|
|
comet::launch::Error::IoFailed(error) => {
|
2025-04-28 09:48:55 +02:00
|
|
|
log::error!("comet failed to run: {error}");
|
2025-04-06 17:21:20 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Task::none()
|
|
|
|
|
}
|
|
|
|
|
Message::InstallComet => {
|
|
|
|
|
self.mode =
|
|
|
|
|
Mode::Setup(Setup::Running { logs: Vec::new() });
|
|
|
|
|
|
2025-04-28 09:48:55 +02:00
|
|
|
comet::install()
|
2025-04-28 22:31:13 +02:00
|
|
|
.map(Message::Installing)
|
2025-04-28 09:48:55 +02:00
|
|
|
.map(Event::Message)
|
2025-04-06 17:21:20 +02:00
|
|
|
}
|
|
|
|
|
|
2025-04-28 22:31:13 +02:00
|
|
|
Message::Installing(Ok(installation)) => {
|
2025-04-28 09:48:55 +02:00
|
|
|
let Mode::Setup(Setup::Running { logs }) = &mut self.mode
|
|
|
|
|
else {
|
|
|
|
|
return Task::none();
|
|
|
|
|
};
|
2025-04-05 20:08:54 +02:00
|
|
|
|
2025-04-28 09:48:55 +02:00
|
|
|
match installation {
|
2025-04-28 22:31:13 +02:00
|
|
|
comet::install::Event::Logged(log) => {
|
2025-04-28 09:48:55 +02:00
|
|
|
logs.push(log);
|
|
|
|
|
Task::none()
|
|
|
|
|
}
|
2025-04-28 22:31:13 +02:00
|
|
|
comet::install::Event::Finished => {
|
2025-05-28 19:58:43 +02:00
|
|
|
self.mode = Mode::Hidden;
|
2025-04-28 09:48:55 +02:00
|
|
|
comet::launch().discard()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-04-28 22:31:13 +02:00
|
|
|
Message::Installing(Err(error)) => {
|
|
|
|
|
let Mode::Setup(Setup::Running { logs }) = &mut self.mode
|
|
|
|
|
else {
|
|
|
|
|
return Task::none();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
match error {
|
|
|
|
|
comet::install::Error::ProcessFailed(status) => {
|
|
|
|
|
logs.push(format!("process failed with {status}"));
|
|
|
|
|
}
|
|
|
|
|
comet::install::Error::IoFailed(error) => {
|
|
|
|
|
logs.push(error.to_string());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-06 17:21:20 +02:00
|
|
|
Task::none()
|
|
|
|
|
}
|
|
|
|
|
Message::CancelSetup => {
|
2025-05-28 19:58:43 +02:00
|
|
|
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;
|
|
|
|
|
}
|
2025-04-06 17:21:20 +02:00
|
|
|
|
|
|
|
|
Task::none()
|
|
|
|
|
}
|
2025-05-28 19:58:43 +02:00
|
|
|
Message::Record => {
|
|
|
|
|
let (state, task) = program.boot();
|
|
|
|
|
|
|
|
|
|
self.state = state;
|
|
|
|
|
self.mode = Mode::Open {
|
|
|
|
|
recorder: Recorder::Recording { events: Vec::new() },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
task.map(Event::Program)
|
|
|
|
|
}
|
2025-04-06 17:21:20 +02:00
|
|
|
},
|
|
|
|
|
Event::Program(message) => {
|
2025-04-21 05:12:08 +02:00
|
|
|
self.time_machine.push(&message);
|
2025-04-20 21:50:12 +02:00
|
|
|
|
2025-04-21 05:12:08 +02:00
|
|
|
if self.time_machine.is_rewinding() {
|
|
|
|
|
debug::enable();
|
2025-04-17 03:24:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let span = debug::update(&message);
|
|
|
|
|
let task = program.update(&mut self.state, message);
|
|
|
|
|
debug::tasks_spawned(task.units());
|
|
|
|
|
span.finish();
|
|
|
|
|
|
2025-04-20 22:11:24 +02:00
|
|
|
if self.time_machine.is_rewinding() {
|
2025-04-20 21:50:12 +02:00
|
|
|
debug::disable();
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-17 03:24:17 +02:00
|
|
|
task.map(Event::Program)
|
|
|
|
|
}
|
|
|
|
|
Event::Command(command) => {
|
|
|
|
|
match command {
|
|
|
|
|
debug::Command::RewindTo { message } => {
|
2025-04-20 22:11:24 +02:00
|
|
|
self.time_machine.rewind(program, message);
|
2025-04-17 03:24:17 +02:00
|
|
|
}
|
2025-04-20 21:50:12 +02:00
|
|
|
debug::Command::GoLive => {
|
2025-04-20 22:11:24 +02:00
|
|
|
self.time_machine.go_to_present();
|
2025-04-20 21:50:12 +02:00
|
|
|
}
|
2025-04-17 03:24:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Task::none()
|
2025-04-05 20:08:54 +02:00
|
|
|
}
|
2025-04-20 21:50:12 +02:00
|
|
|
Event::Discard => Task::none(),
|
2025-04-05 19:27:15 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-20 22:11:24 +02:00
|
|
|
fn view(
|
2025-04-05 19:27:15 +02:00
|
|
|
&self,
|
|
|
|
|
program: &P,
|
|
|
|
|
window: window::Id,
|
2025-04-06 17:21:20 +02:00
|
|
|
) -> Element<'_, Event<P>, P::Theme, P::Renderer> {
|
2025-04-20 22:11:24 +02:00
|
|
|
let state = self.state();
|
2025-04-17 03:24:17 +02:00
|
|
|
|
2025-04-20 21:50:12 +02:00
|
|
|
let view = {
|
|
|
|
|
let view = program.view(state, window);
|
2025-05-28 19:58:43 +02:00
|
|
|
let theme = program.theme(state, window);
|
|
|
|
|
|
|
|
|
|
let view: Element<'_, _, Theme, _> = themer(theme, view).into();
|
2025-04-20 21:50:12 +02:00
|
|
|
|
2025-04-20 22:11:24 +02:00
|
|
|
if self.time_machine.is_rewinding() {
|
2025-04-20 21:50:12 +02:00
|
|
|
view.map(|_| Event::Discard)
|
|
|
|
|
} else {
|
|
|
|
|
view.map(Event::Program)
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-05-28 19:58:43 +02:00
|
|
|
let theme = program
|
|
|
|
|
.theme(state, window)
|
|
|
|
|
.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 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)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
.map(|mode| Element::from(mode).map(Event::Message));
|
2025-04-05 20:08:54 +02:00
|
|
|
|
2025-05-28 19:58:43 +02:00
|
|
|
let notification = self.show_notification.then(|| {
|
|
|
|
|
bottom_right(opaque(
|
|
|
|
|
container(text("Press F12 to open developer tools"))
|
|
|
|
|
.padding(10)
|
|
|
|
|
.style(container::dark),
|
|
|
|
|
))
|
|
|
|
|
});
|
2025-04-06 17:21:20 +02:00
|
|
|
|
2025-05-28 19:58:43 +02:00
|
|
|
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)
|
2025-04-06 17:21:20 +02:00
|
|
|
};
|
|
|
|
|
|
2025-05-28 19:58:43 +02:00
|
|
|
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)
|
2025-04-06 17:21:20 +02:00
|
|
|
.padding(10)
|
2025-05-28 19:58:43 +02:00
|
|
|
.width(250)
|
|
|
|
|
.height(Fill)
|
|
|
|
|
.style(container::dark);
|
2025-04-06 17:21:20 +02:00
|
|
|
|
2025-05-28 19:58:43 +02:00
|
|
|
Some(Element::from(sidebar).map(Event::Message))
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
2025-04-06 17:21:20 +02:00
|
|
|
|
2025-05-28 19:58:43 +02:00
|
|
|
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(),
|
|
|
|
|
}),
|
2025-04-06 17:21:20 +02:00
|
|
|
)
|
2025-05-28 19:58:43 +02:00
|
|
|
.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);
|
2025-04-05 20:08:54 +02:00
|
|
|
|
2025-05-28 19:58:43 +02:00
|
|
|
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()
|
2025-04-05 19:27:15 +02:00
|
|
|
}
|
|
|
|
|
|
2025-04-20 22:11:24 +02:00
|
|
|
fn subscription(&self, program: &P) -> Subscription<Event<P>> {
|
2025-04-05 20:08:54 +02:00
|
|
|
let subscription =
|
2025-04-06 17:21:20 +02:00
|
|
|
program.subscription(&self.state).map(Event::Program);
|
2025-04-17 03:24:17 +02:00
|
|
|
debug::subscriptions_tracked(subscription.units());
|
|
|
|
|
|
2025-04-05 20:08:54 +02:00
|
|
|
let hotkeys =
|
|
|
|
|
futures::keyboard::on_key_press(|key, _modifiers| match key {
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::F12) => {
|
2025-05-28 19:58:43 +02:00
|
|
|
Some(Message::Toggle)
|
|
|
|
|
}
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::F11) => {
|
2025-04-05 20:08:54 +02:00
|
|
|
Some(Message::ToggleComet)
|
|
|
|
|
}
|
|
|
|
|
_ => None,
|
2025-04-06 17:21:20 +02:00
|
|
|
})
|
|
|
|
|
.map(Event::Message);
|
2025-04-05 20:08:54 +02:00
|
|
|
|
2025-04-17 03:24:17 +02:00
|
|
|
let commands = debug::commands().map(Event::Command);
|
|
|
|
|
|
|
|
|
|
Subscription::batch([subscription, hotkeys, commands])
|
2025-04-05 19:27:15 +02:00
|
|
|
}
|
|
|
|
|
|
2025-04-20 22:11:24 +02:00
|
|
|
fn theme(&self, program: &P, window: window::Id) -> P::Theme {
|
|
|
|
|
program.theme(self.state(), window)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn style(&self, program: &P, theme: &P::Theme) -> theme::Style {
|
|
|
|
|
program.style(self.state(), theme)
|
2025-04-05 19:27:15 +02:00
|
|
|
}
|
|
|
|
|
|
2025-04-20 22:11:24 +02:00
|
|
|
fn scale_factor(&self, program: &P, window: window::Id) -> f64 {
|
|
|
|
|
program.scale_factor(self.state(), window)
|
2025-04-05 19:27:15 +02:00
|
|
|
}
|
|
|
|
|
|
2025-04-20 22:11:24 +02:00
|
|
|
fn state(&self) -> &P::State {
|
|
|
|
|
self.time_machine.state().unwrap_or(&self.state)
|
2025-04-05 19:27:15 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-04 22:39:23 +02:00
|
|
|
pub enum Event<P>
|
2025-04-05 19:27:15 +02:00
|
|
|
where
|
|
|
|
|
P: Program,
|
|
|
|
|
{
|
2025-04-06 17:21:20 +02:00
|
|
|
Message(Message),
|
2025-04-05 19:27:15 +02:00
|
|
|
Program(P::Message),
|
2025-04-17 03:24:17 +02:00
|
|
|
Command(debug::Command),
|
2025-04-20 21:50:12 +02:00
|
|
|
Discard,
|
2025-04-05 19:27:15 +02:00
|
|
|
}
|
|
|
|
|
|
2025-04-06 17:21:20 +02:00
|
|
|
impl<P> fmt::Debug for Event<P>
|
2025-04-05 19:27:15 +02:00
|
|
|
where
|
|
|
|
|
P: Program,
|
|
|
|
|
{
|
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
|
match self {
|
2025-04-06 17:21:20 +02:00
|
|
|
Self::Message(message) => message.fmt(f),
|
|
|
|
|
Self::Program(message) => message.fmt(f),
|
2025-04-17 03:24:17 +02:00
|
|
|
Self::Command(command) => command.fmt(f),
|
2025-04-20 21:50:12 +02:00
|
|
|
Self::Discard => f.write_str("Discard"),
|
2025-04-17 03:24:17 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "time-travel")]
|
|
|
|
|
impl<P> Clone for Event<P>
|
|
|
|
|
where
|
|
|
|
|
P: Program,
|
|
|
|
|
{
|
|
|
|
|
fn clone(&self) -> Self {
|
|
|
|
|
match self {
|
2025-04-20 21:50:12 +02:00
|
|
|
Self::Message(message) => Self::Message(message.clone()),
|
|
|
|
|
Self::Program(message) => Self::Program(message.clone()),
|
|
|
|
|
Self::Command(command) => Self::Command(*command),
|
|
|
|
|
Self::Discard => Self::Discard,
|
2025-04-05 19:27:15 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-04-28 22:31:13 +02:00
|
|
|
|
|
|
|
|
fn setup<Renderer>(goal: &Goal) -> Element<'_, Message, Theme, Renderer>
|
|
|
|
|
where
|
2025-05-28 19:58:43 +02:00
|
|
|
Renderer: program::Renderer + 'static,
|
2025-04-28 22:31:13 +02:00
|
|
|
{
|
|
|
|
|
let controls = row![
|
|
|
|
|
button(text("Cancel").center().width(Fill))
|
|
|
|
|
.width(100)
|
|
|
|
|
.on_press(Message::CancelSetup)
|
|
|
|
|
.style(button::danger),
|
|
|
|
|
horizontal_space(),
|
|
|
|
|
button(
|
|
|
|
|
text(match goal {
|
|
|
|
|
Goal::Installation => "Install",
|
|
|
|
|
Goal::Update { .. } => "Update",
|
|
|
|
|
})
|
|
|
|
|
.center()
|
|
|
|
|
.width(Fill)
|
|
|
|
|
)
|
|
|
|
|
.width(100)
|
|
|
|
|
.on_press(Message::InstallComet)
|
|
|
|
|
.style(button::success),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
let command = container(
|
2025-05-28 19:58:43 +02:00
|
|
|
monospace(format!(
|
2025-04-28 22:31:13 +02:00
|
|
|
"cargo install --locked \\
|
|
|
|
|
--git https://github.com/iced-rs/comet.git \\
|
|
|
|
|
--rev {}",
|
|
|
|
|
comet::COMPATIBLE_REVISION
|
2025-05-28 19:58:43 +02:00
|
|
|
))
|
|
|
|
|
.size(14),
|
2025-04-28 22:31:13 +02:00
|
|
|
)
|
|
|
|
|
.width(Fill)
|
|
|
|
|
.padding(5)
|
|
|
|
|
.style(container::dark);
|
|
|
|
|
|
|
|
|
|
Element::from(match goal {
|
|
|
|
|
Goal::Installation => column![
|
|
|
|
|
text("comet is not installed!").size(20),
|
|
|
|
|
"In order to display performance \
|
|
|
|
|
metrics, the comet debugger must \
|
|
|
|
|
be installed in your system.",
|
|
|
|
|
"The comet debugger is an official \
|
|
|
|
|
companion tool that helps you debug \
|
|
|
|
|
your iced applications.",
|
|
|
|
|
column![
|
|
|
|
|
"Do you wish to install it with the \
|
|
|
|
|
following command?",
|
|
|
|
|
command
|
|
|
|
|
]
|
|
|
|
|
.spacing(10),
|
|
|
|
|
controls,
|
|
|
|
|
]
|
|
|
|
|
.spacing(20),
|
|
|
|
|
Goal::Update { revision } => {
|
|
|
|
|
let comparison = column![
|
|
|
|
|
row![
|
|
|
|
|
"Installed revision:",
|
|
|
|
|
horizontal_space(),
|
|
|
|
|
inline_code(revision.as_deref().unwrap_or("Unknown"))
|
|
|
|
|
]
|
|
|
|
|
.align_y(Center),
|
|
|
|
|
row![
|
|
|
|
|
"Compatible revision:",
|
|
|
|
|
horizontal_space(),
|
|
|
|
|
inline_code(comet::COMPATIBLE_REVISION),
|
|
|
|
|
]
|
|
|
|
|
.align_y(Center)
|
|
|
|
|
]
|
|
|
|
|
.spacing(5);
|
|
|
|
|
|
|
|
|
|
column![
|
|
|
|
|
text("comet is out of date!").size(20),
|
|
|
|
|
comparison,
|
|
|
|
|
column![
|
|
|
|
|
"Do you wish to update it with the following \
|
|
|
|
|
command?",
|
|
|
|
|
command
|
|
|
|
|
]
|
|
|
|
|
.spacing(10),
|
|
|
|
|
controls,
|
|
|
|
|
]
|
|
|
|
|
.spacing(20)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn installation<'a, Renderer>(
|
|
|
|
|
logs: &'a [String],
|
|
|
|
|
) -> Element<'a, Message, Theme, Renderer>
|
|
|
|
|
where
|
2025-05-28 19:58:43 +02:00
|
|
|
Renderer: program::Renderer + 'a,
|
2025-04-28 22:31:13 +02:00
|
|
|
{
|
|
|
|
|
column![
|
|
|
|
|
text("Installing comet...").size(20),
|
|
|
|
|
container(
|
|
|
|
|
scrollable(
|
2025-05-28 19:58:43 +02:00
|
|
|
column(
|
|
|
|
|
logs.iter().map(|log| { monospace(log).size(12).into() }),
|
|
|
|
|
)
|
2025-04-28 22:31:13 +02:00
|
|
|
.spacing(3),
|
|
|
|
|
)
|
|
|
|
|
.spacing(10)
|
|
|
|
|
.width(Fill)
|
|
|
|
|
.height(300)
|
|
|
|
|
.anchor_bottom(),
|
|
|
|
|
)
|
|
|
|
|
.padding(10)
|
|
|
|
|
.style(container::dark)
|
|
|
|
|
]
|
|
|
|
|
.spacing(20)
|
|
|
|
|
.into()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn inline_code<'a, Renderer>(
|
|
|
|
|
code: impl text::IntoFragment<'a>,
|
|
|
|
|
) -> Element<'a, Message, Theme, Renderer>
|
|
|
|
|
where
|
2025-05-28 19:58:43 +02:00
|
|
|
Renderer: program::Renderer + 'a,
|
2025-04-28 22:31:13 +02:00
|
|
|
{
|
2025-05-28 19:58:43 +02:00
|
|
|
container(monospace(code).size(12))
|
2025-04-28 22:31:13 +02:00
|
|
|
.style(|_theme| {
|
|
|
|
|
container::Style::default()
|
|
|
|
|
.background(Color::BLACK)
|
|
|
|
|
.border(border::rounded(2))
|
|
|
|
|
})
|
|
|
|
|
.padding([2, 4])
|
|
|
|
|
.into()
|
|
|
|
|
}
|
2025-05-28 19:58:43 +02:00
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
}
|