1#![allow(missing_docs)]
2use iced_debug as debug;
3use iced_program as program;
4use iced_widget as widget;
5use iced_widget::core;
6use iced_widget::runtime;
7use iced_widget::runtime::futures;
8
9mod comet;
10mod executor;
11mod time_machine;
12
13use crate::core::border;
14use crate::core::keyboard;
15use crate::core::theme::{self, Base, Theme};
16use crate::core::time::seconds;
17use crate::core::window;
18use crate::core::{Alignment::Center, Color, Element, Length::Fill};
19use crate::futures::Subscription;
20use crate::program::Program;
21use crate::runtime::Task;
22use crate::time_machine::TimeMachine;
23use crate::widget::{
24 bottom_right, button, center, column, container, horizontal_space, opaque,
25 row, scrollable, stack, text, themer,
26};
27
28use std::fmt;
29use std::thread;
30
31pub fn attach<P: Program + 'static>(program: P) -> Attach<P> {
32 Attach { program }
33}
34
35#[derive(Debug)]
37pub struct Attach<P> {
38 pub program: P,
40}
41
42impl<P> Program for Attach<P>
43where
44 P: Program + 'static,
45{
46 type State = DevTools<P>;
47 type Message = Event<P>;
48 type Theme = P::Theme;
49 type Renderer = P::Renderer;
50 type Executor = P::Executor;
51
52 fn name() -> &'static str {
53 P::name()
54 }
55
56 fn boot(&self) -> (Self::State, Task<Self::Message>) {
57 let (state, boot) = self.program.boot();
58 let (devtools, task) = DevTools::new(state);
59
60 (
61 devtools,
62 Task::batch([boot.map(Event::Program), task.map(Event::Message)]),
63 )
64 }
65
66 fn update(
67 &self,
68 state: &mut Self::State,
69 message: Self::Message,
70 ) -> Task<Self::Message> {
71 state.update(&self.program, message)
72 }
73
74 fn view<'a>(
75 &self,
76 state: &'a Self::State,
77 window: window::Id,
78 ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> {
79 state.view(&self.program, window)
80 }
81
82 fn title(&self, state: &Self::State, window: window::Id) -> String {
83 state.title(&self.program, window)
84 }
85
86 fn subscription(
87 &self,
88 state: &Self::State,
89 ) -> runtime::futures::Subscription<Self::Message> {
90 state.subscription(&self.program)
91 }
92
93 fn theme(&self, state: &Self::State, window: window::Id) -> Self::Theme {
94 state.theme(&self.program, window)
95 }
96
97 fn style(&self, state: &Self::State, theme: &Self::Theme) -> theme::Style {
98 state.style(&self.program, theme)
99 }
100
101 fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 {
102 state.scale_factor(&self.program, window)
103 }
104}
105
106#[allow(missing_debug_implementations)]
108pub struct DevTools<P>
109where
110 P: Program,
111{
112 state: P::State,
113 mode: Mode,
114 show_notification: bool,
115 time_machine: TimeMachine<P>,
116}
117
118#[derive(Debug, Clone)]
119pub enum Message {
120 HideNotification,
121 ToggleComet,
122 CometLaunched(comet::launch::Result),
123 InstallComet,
124 Installing(comet::install::Result),
125 CancelSetup,
126}
127
128enum Mode {
129 None,
130 Setup(Setup),
131}
132
133enum Setup {
134 Idle { goal: Goal },
135 Running { logs: Vec<String> },
136}
137
138enum Goal {
139 Installation,
140 Update { revision: Option<String> },
141}
142
143impl<P> DevTools<P>
144where
145 P: Program + 'static,
146{
147 fn new(state: P::State) -> (Self, Task<Message>) {
148 (
149 Self {
150 state,
151 mode: Mode::None,
152 show_notification: true,
153 time_machine: TimeMachine::new(),
154 },
155 executor::spawn_blocking(|mut sender| {
156 thread::sleep(seconds(2));
157 let _ = sender.try_send(());
158 })
159 .map(|_| Message::HideNotification),
160 )
161 }
162
163 fn title(&self, program: &P, window: window::Id) -> String {
164 program.title(&self.state, window)
165 }
166
167 fn update(&mut self, program: &P, event: Event<P>) -> Task<Event<P>> {
168 match event {
169 Event::Message(message) => match message {
170 Message::HideNotification => {
171 self.show_notification = false;
172
173 Task::none()
174 }
175 Message::ToggleComet => {
176 if let Mode::Setup(setup) = &self.mode {
177 if matches!(setup, Setup::Idle { .. }) {
178 self.mode = Mode::None;
179 }
180
181 Task::none()
182 } else if debug::quit() {
183 Task::none()
184 } else {
185 comet::launch()
186 .map(Message::CometLaunched)
187 .map(Event::Message)
188 }
189 }
190 Message::CometLaunched(Ok(())) => Task::none(),
191 Message::CometLaunched(Err(error)) => {
192 match error {
193 comet::launch::Error::NotFound => {
194 self.mode = Mode::Setup(Setup::Idle {
195 goal: Goal::Installation,
196 });
197 }
198 comet::launch::Error::Outdated { revision } => {
199 self.mode = Mode::Setup(Setup::Idle {
200 goal: Goal::Update { revision },
201 });
202 }
203 comet::launch::Error::IoFailed(error) => {
204 log::error!("comet failed to run: {error}");
205 }
206 }
207
208 Task::none()
209 }
210 Message::InstallComet => {
211 self.mode =
212 Mode::Setup(Setup::Running { logs: Vec::new() });
213
214 comet::install()
215 .map(Message::Installing)
216 .map(Event::Message)
217 }
218
219 Message::Installing(Ok(installation)) => {
220 let Mode::Setup(Setup::Running { logs }) = &mut self.mode
221 else {
222 return Task::none();
223 };
224
225 match installation {
226 comet::install::Event::Logged(log) => {
227 logs.push(log);
228 Task::none()
229 }
230 comet::install::Event::Finished => {
231 self.mode = Mode::None;
232 comet::launch().discard()
233 }
234 }
235 }
236 Message::Installing(Err(error)) => {
237 let Mode::Setup(Setup::Running { logs }) = &mut self.mode
238 else {
239 return Task::none();
240 };
241
242 match error {
243 comet::install::Error::ProcessFailed(status) => {
244 logs.push(format!("process failed with {status}"));
245 }
246 comet::install::Error::IoFailed(error) => {
247 logs.push(error.to_string());
248 }
249 }
250
251 Task::none()
252 }
253 Message::CancelSetup => {
254 self.mode = Mode::None;
255
256 Task::none()
257 }
258 },
259 Event::Program(message) => {
260 self.time_machine.push(&message);
261
262 if self.time_machine.is_rewinding() {
263 debug::enable();
264 }
265
266 let span = debug::update(&message);
267 let task = program.update(&mut self.state, message);
268 debug::tasks_spawned(task.units());
269 span.finish();
270
271 if self.time_machine.is_rewinding() {
272 debug::disable();
273 }
274
275 task.map(Event::Program)
276 }
277 Event::Command(command) => {
278 match command {
279 debug::Command::RewindTo { message } => {
280 self.time_machine.rewind(program, message);
281 }
282 debug::Command::GoLive => {
283 self.time_machine.go_to_present();
284 }
285 }
286
287 Task::none()
288 }
289 Event::Discard => Task::none(),
290 }
291 }
292
293 fn view(
294 &self,
295 program: &P,
296 window: window::Id,
297 ) -> Element<'_, Event<P>, P::Theme, P::Renderer> {
298 let state = self.state();
299
300 let view = {
301 let view = program.view(state, window);
302
303 if self.time_machine.is_rewinding() {
304 view.map(|_| Event::Discard)
305 } else {
306 view.map(Event::Program)
307 }
308 };
309
310 let theme = program.theme(state, window);
311
312 let derive_theme = move || {
313 theme
314 .palette()
315 .map(|palette| Theme::custom("iced devtools", palette))
316 .unwrap_or_default()
317 };
318
319 let mode = match &self.mode {
320 Mode::None => None,
321 Mode::Setup(setup) => {
322 let stage: Element<'_, _, Theme, P::Renderer> = match setup {
323 Setup::Idle { goal } => self::setup(goal),
324 Setup::Running { logs } => installation(logs),
325 };
326
327 let setup = center(
328 container(stage)
329 .padding(20)
330 .max_width(500)
331 .style(container::bordered_box),
332 )
333 .padding(10)
334 .style(|_theme| {
335 container::Style::default()
336 .background(Color::BLACK.scale_alpha(0.8))
337 });
338
339 Some(setup)
340 }
341 }
342 .map(|mode| {
343 themer(derive_theme(), Element::from(mode).map(Event::Message))
344 });
345
346 let notification = self.show_notification.then(|| {
347 themer(
348 derive_theme(),
349 bottom_right(opaque(
350 container(text("Press F12 to open debug metrics"))
351 .padding(10)
352 .style(container::dark),
353 )),
354 )
355 });
356
357 stack![view]
358 .height(Fill)
359 .push_maybe(mode.map(opaque))
360 .push_maybe(notification)
361 .into()
362 }
363
364 fn subscription(&self, program: &P) -> Subscription<Event<P>> {
365 let subscription =
366 program.subscription(&self.state).map(Event::Program);
367 debug::subscriptions_tracked(subscription.units());
368
369 let hotkeys =
370 futures::keyboard::on_key_press(|key, _modifiers| match key {
371 keyboard::Key::Named(keyboard::key::Named::F12) => {
372 Some(Message::ToggleComet)
373 }
374 _ => None,
375 })
376 .map(Event::Message);
377
378 let commands = debug::commands().map(Event::Command);
379
380 Subscription::batch([subscription, hotkeys, commands])
381 }
382
383 fn theme(&self, program: &P, window: window::Id) -> P::Theme {
384 program.theme(self.state(), window)
385 }
386
387 fn style(&self, program: &P, theme: &P::Theme) -> theme::Style {
388 program.style(self.state(), theme)
389 }
390
391 fn scale_factor(&self, program: &P, window: window::Id) -> f64 {
392 program.scale_factor(self.state(), window)
393 }
394
395 fn state(&self) -> &P::State {
396 self.time_machine.state().unwrap_or(&self.state)
397 }
398}
399
400pub enum Event<P>
401where
402 P: Program,
403{
404 Message(Message),
405 Program(P::Message),
406 Command(debug::Command),
407 Discard,
408}
409
410impl<P> fmt::Debug for Event<P>
411where
412 P: Program,
413{
414 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
415 match self {
416 Self::Message(message) => message.fmt(f),
417 Self::Program(message) => message.fmt(f),
418 Self::Command(command) => command.fmt(f),
419 Self::Discard => f.write_str("Discard"),
420 }
421 }
422}
423
424#[cfg(feature = "time-travel")]
425impl<P> Clone for Event<P>
426where
427 P: Program,
428{
429 fn clone(&self) -> Self {
430 match self {
431 Self::Message(message) => Self::Message(message.clone()),
432 Self::Program(message) => Self::Program(message.clone()),
433 Self::Command(command) => Self::Command(*command),
434 Self::Discard => Self::Discard,
435 }
436 }
437}
438
439fn setup<Renderer>(goal: &Goal) -> Element<'_, Message, Theme, Renderer>
440where
441 Renderer: core::text::Renderer + 'static,
442{
443 let controls = row![
444 button(text("Cancel").center().width(Fill))
445 .width(100)
446 .on_press(Message::CancelSetup)
447 .style(button::danger),
448 horizontal_space(),
449 button(
450 text(match goal {
451 Goal::Installation => "Install",
452 Goal::Update { .. } => "Update",
453 })
454 .center()
455 .width(Fill)
456 )
457 .width(100)
458 .on_press(Message::InstallComet)
459 .style(button::success),
460 ];
461
462 let command = container(
463 text!(
464 "cargo install --locked \\
465 --git https://github.com/iced-rs/comet.git \\
466 --rev {}",
467 comet::COMPATIBLE_REVISION
468 )
469 .size(14)
470 .font(Renderer::MONOSPACE_FONT),
471 )
472 .width(Fill)
473 .padding(5)
474 .style(container::dark);
475
476 Element::from(match goal {
477 Goal::Installation => column![
478 text("comet is not installed!").size(20),
479 "In order to display performance \
480 metrics, the comet debugger must \
481 be installed in your system.",
482 "The comet debugger is an official \
483 companion tool that helps you debug \
484 your iced applications.",
485 column![
486 "Do you wish to install it with the \
487 following command?",
488 command
489 ]
490 .spacing(10),
491 controls,
492 ]
493 .spacing(20),
494 Goal::Update { revision } => {
495 let comparison = column![
496 row![
497 "Installed revision:",
498 horizontal_space(),
499 inline_code(revision.as_deref().unwrap_or("Unknown"))
500 ]
501 .align_y(Center),
502 row![
503 "Compatible revision:",
504 horizontal_space(),
505 inline_code(comet::COMPATIBLE_REVISION),
506 ]
507 .align_y(Center)
508 ]
509 .spacing(5);
510
511 column![
512 text("comet is out of date!").size(20),
513 comparison,
514 column![
515 "Do you wish to update it with the following \
516 command?",
517 command
518 ]
519 .spacing(10),
520 controls,
521 ]
522 .spacing(20)
523 }
524 })
525}
526
527fn installation<'a, Renderer>(
528 logs: &'a [String],
529) -> Element<'a, Message, Theme, Renderer>
530where
531 Renderer: core::text::Renderer + 'a,
532{
533 column![
534 text("Installing comet...").size(20),
535 container(
536 scrollable(
537 column(logs.iter().map(|log| {
538 text(log).size(12).font(Renderer::MONOSPACE_FONT).into()
539 }),)
540 .spacing(3),
541 )
542 .spacing(10)
543 .width(Fill)
544 .height(300)
545 .anchor_bottom(),
546 )
547 .padding(10)
548 .style(container::dark)
549 ]
550 .spacing(20)
551 .into()
552}
553
554fn inline_code<'a, Renderer>(
555 code: impl text::IntoFragment<'a>,
556) -> Element<'a, Message, Theme, Renderer>
557where
558 Renderer: core::text::Renderer + 'a,
559{
560 container(text(code).font(Renderer::MONOSPACE_FONT).size(12))
561 .style(|_theme| {
562 container::Style::default()
563 .background(Color::BLACK)
564 .border(border::rounded(2))
565 })
566 .padding([2, 4])
567 .into()
568}