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
347 .show_notification
348 .then(|| text("Press F12 to open debug metrics"))
349 .or_else(|| {
350 debug::is_stale().then(|| {
351 text(
352 "Types have changed. Restart to re-enable hotpatching.",
353 )
354 })
355 });
356
357 stack![view]
358 .height(Fill)
359 .push_maybe(mode.map(opaque))
360 .push_maybe(notification.map(|notification| {
361 themer(
362 derive_theme(),
363 bottom_right(opaque(
364 container(notification)
365 .padding(10)
366 .style(container::dark),
367 )),
368 )
369 }))
370 .into()
371 }
372
373 fn subscription(&self, program: &P) -> Subscription<Event<P>> {
374 let subscription =
375 program.subscription(&self.state).map(Event::Program);
376 debug::subscriptions_tracked(subscription.units());
377
378 let hotkeys =
379 futures::keyboard::on_key_press(|key, _modifiers| match key {
380 keyboard::Key::Named(keyboard::key::Named::F12) => {
381 Some(Message::ToggleComet)
382 }
383 _ => None,
384 })
385 .map(Event::Message);
386
387 let commands = debug::commands().map(Event::Command);
388
389 Subscription::batch([subscription, hotkeys, commands])
390 }
391
392 fn theme(&self, program: &P, window: window::Id) -> P::Theme {
393 program.theme(self.state(), window)
394 }
395
396 fn style(&self, program: &P, theme: &P::Theme) -> theme::Style {
397 program.style(self.state(), theme)
398 }
399
400 fn scale_factor(&self, program: &P, window: window::Id) -> f64 {
401 program.scale_factor(self.state(), window)
402 }
403
404 fn state(&self) -> &P::State {
405 self.time_machine.state().unwrap_or(&self.state)
406 }
407}
408
409pub enum Event<P>
410where
411 P: Program,
412{
413 Message(Message),
414 Program(P::Message),
415 Command(debug::Command),
416 Discard,
417}
418
419impl<P> fmt::Debug for Event<P>
420where
421 P: Program,
422{
423 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
424 match self {
425 Self::Message(message) => message.fmt(f),
426 Self::Program(message) => message.fmt(f),
427 Self::Command(command) => command.fmt(f),
428 Self::Discard => f.write_str("Discard"),
429 }
430 }
431}
432
433#[cfg(feature = "time-travel")]
434impl<P> Clone for Event<P>
435where
436 P: Program,
437{
438 fn clone(&self) -> Self {
439 match self {
440 Self::Message(message) => Self::Message(message.clone()),
441 Self::Program(message) => Self::Program(message.clone()),
442 Self::Command(command) => Self::Command(*command),
443 Self::Discard => Self::Discard,
444 }
445 }
446}
447
448fn setup<Renderer>(goal: &Goal) -> Element<'_, Message, Theme, Renderer>
449where
450 Renderer: core::text::Renderer + 'static,
451{
452 let controls = row![
453 button(text("Cancel").center().width(Fill))
454 .width(100)
455 .on_press(Message::CancelSetup)
456 .style(button::danger),
457 horizontal_space(),
458 button(
459 text(match goal {
460 Goal::Installation => "Install",
461 Goal::Update { .. } => "Update",
462 })
463 .center()
464 .width(Fill)
465 )
466 .width(100)
467 .on_press(Message::InstallComet)
468 .style(button::success),
469 ];
470
471 let command = container(
472 text!(
473 "cargo install --locked \\
474 --git https://github.com/iced-rs/comet.git \\
475 --rev {}",
476 comet::COMPATIBLE_REVISION
477 )
478 .size(14)
479 .font(Renderer::MONOSPACE_FONT),
480 )
481 .width(Fill)
482 .padding(5)
483 .style(container::dark);
484
485 Element::from(match goal {
486 Goal::Installation => column![
487 text("comet is not installed!").size(20),
488 "In order to display performance \
489 metrics, the comet debugger must \
490 be installed in your system.",
491 "The comet debugger is an official \
492 companion tool that helps you debug \
493 your iced applications.",
494 column![
495 "Do you wish to install it with the \
496 following command?",
497 command
498 ]
499 .spacing(10),
500 controls,
501 ]
502 .spacing(20),
503 Goal::Update { revision } => {
504 let comparison = column![
505 row![
506 "Installed revision:",
507 horizontal_space(),
508 inline_code(revision.as_deref().unwrap_or("Unknown"))
509 ]
510 .align_y(Center),
511 row![
512 "Compatible revision:",
513 horizontal_space(),
514 inline_code(comet::COMPATIBLE_REVISION),
515 ]
516 .align_y(Center)
517 ]
518 .spacing(5);
519
520 column![
521 text("comet is out of date!").size(20),
522 comparison,
523 column![
524 "Do you wish to update it with the following \
525 command?",
526 command
527 ]
528 .spacing(10),
529 controls,
530 ]
531 .spacing(20)
532 }
533 })
534}
535
536fn installation<'a, Renderer>(
537 logs: &'a [String],
538) -> Element<'a, Message, Theme, Renderer>
539where
540 Renderer: core::text::Renderer + 'a,
541{
542 column![
543 text("Installing comet...").size(20),
544 container(
545 scrollable(
546 column(logs.iter().map(|log| {
547 text(log).size(12).font(Renderer::MONOSPACE_FONT).into()
548 }),)
549 .spacing(3),
550 )
551 .spacing(10)
552 .width(Fill)
553 .height(300)
554 .anchor_bottom(),
555 )
556 .padding(10)
557 .style(container::dark)
558 ]
559 .spacing(20)
560 .into()
561}
562
563fn inline_code<'a, Renderer>(
564 code: impl text::IntoFragment<'a>,
565) -> Element<'a, Message, Theme, Renderer>
566where
567 Renderer: core::text::Renderer + 'a,
568{
569 container(text(code).font(Renderer::MONOSPACE_FONT).size(12))
570 .style(|_theme| {
571 container::Style::default()
572 .background(Color::BLACK)
573 .border(border::rounded(2))
574 })
575 .padding([2, 4])
576 .into()
577}