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