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