1mod breakpoint_list;
2mod console;
3mod loaded_source_list;
4mod module_list;
5pub mod stack_frame_list;
6pub mod variable_list;
7
8use std::{any::Any, ops::ControlFlow, sync::Arc};
9
10use super::DebugPanelItemEvent;
11use breakpoint_list::BreakpointList;
12use collections::HashMap;
13use console::Console;
14use dap::{Capabilities, Thread, client::SessionId, debugger_settings::DebuggerSettings};
15use gpui::{
16 Action as _, AnyView, AppContext, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
17 NoAction, Subscription, WeakEntity,
18};
19use loaded_source_list::LoadedSourceList;
20use module_list::ModuleList;
21use project::{
22 Project,
23 debugger::session::{Session, SessionEvent, ThreadId, ThreadStatus},
24};
25use rpc::proto::ViewId;
26use settings::Settings;
27use stack_frame_list::StackFrameList;
28use ui::{
29 ActiveTheme, AnyElement, App, Context, ContextMenu, DropdownMenu, FluentBuilder,
30 InteractiveElement, IntoElement, Label, LabelCommon as _, ParentElement, Render, SharedString,
31 StatefulInteractiveElement, Styled, Tab, Window, div, h_flex, v_flex,
32};
33use util::ResultExt;
34use variable_list::VariableList;
35use workspace::{
36 ActivePaneDecorator, DraggedTab, Item, Pane, PaneGroup, Workspace, item::TabContentParams,
37 move_item, pane::Event,
38};
39
40pub struct RunningState {
41 session: Entity<Session>,
42 thread_id: Option<ThreadId>,
43 focus_handle: FocusHandle,
44 _remote_id: Option<ViewId>,
45 workspace: WeakEntity<Workspace>,
46 session_id: SessionId,
47 variable_list: Entity<variable_list::VariableList>,
48 _subscriptions: Vec<Subscription>,
49 stack_frame_list: Entity<stack_frame_list::StackFrameList>,
50 _module_list: Entity<module_list::ModuleList>,
51 _console: Entity<Console>,
52 panes: PaneGroup,
53 pane_close_subscriptions: HashMap<EntityId, Subscription>,
54}
55
56impl Render for RunningState {
57 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
58 let active = self.panes.panes().into_iter().next();
59 let x = if let Some(active) = active {
60 self.panes
61 .render(
62 None,
63 &ActivePaneDecorator::new(active, &self.workspace),
64 window,
65 cx,
66 )
67 .into_any_element()
68 } else {
69 div().into_any_element()
70 };
71 let thread_status = self
72 .thread_id
73 .map(|thread_id| self.session.read(cx).thread_status(thread_id))
74 .unwrap_or(ThreadStatus::Exited);
75
76 self.variable_list.update(cx, |this, cx| {
77 this.disabled(thread_status != ThreadStatus::Stopped, cx);
78 });
79 v_flex()
80 .size_full()
81 .key_context("DebugSessionItem")
82 .track_focus(&self.focus_handle(cx))
83 .child(h_flex().flex_1().child(x))
84 }
85}
86
87struct SubView {
88 inner: AnyView,
89 pane_focus_handle: FocusHandle,
90 tab_name: SharedString,
91 show_indicator: Box<dyn Fn(&App) -> bool>,
92}
93
94impl SubView {
95 fn new(
96 pane_focus_handle: FocusHandle,
97 view: AnyView,
98 tab_name: SharedString,
99 show_indicator: Option<Box<dyn Fn(&App) -> bool>>,
100 cx: &mut App,
101 ) -> Entity<Self> {
102 cx.new(|_| Self {
103 tab_name,
104 inner: view,
105 pane_focus_handle,
106 show_indicator: show_indicator.unwrap_or(Box::new(|_| false)),
107 })
108 }
109}
110impl Focusable for SubView {
111 fn focus_handle(&self, _: &App) -> FocusHandle {
112 self.pane_focus_handle.clone()
113 }
114}
115impl EventEmitter<()> for SubView {}
116impl Item for SubView {
117 type Event = ();
118
119 fn tab_content(
120 &self,
121 params: workspace::item::TabContentParams,
122 _: &Window,
123 cx: &App,
124 ) -> AnyElement {
125 let label = Label::new(self.tab_name.clone())
126 .size(ui::LabelSize::Small)
127 .color(params.text_color())
128 .line_height_style(ui::LineHeightStyle::UiLabel);
129
130 if !params.selected && self.show_indicator.as_ref()(cx) {
131 return h_flex()
132 .justify_between()
133 .child(ui::Indicator::dot())
134 .gap_2()
135 .child(label)
136 .into_any_element();
137 }
138
139 label.into_any_element()
140 }
141}
142
143impl Render for SubView {
144 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
145 v_flex().size_full().child(self.inner.clone())
146 }
147}
148
149fn new_debugger_pane(
150 workspace: WeakEntity<Workspace>,
151 project: Entity<Project>,
152 window: &mut Window,
153 cx: &mut Context<RunningState>,
154) -> Entity<Pane> {
155 let weak_running = cx.weak_entity();
156 let custom_drop_handle = {
157 let workspace = workspace.clone();
158 let project = project.downgrade();
159 let weak_running = weak_running.clone();
160 move |pane: &mut Pane, any: &dyn Any, window: &mut Window, cx: &mut Context<Pane>| {
161 let Some(tab) = any.downcast_ref::<DraggedTab>() else {
162 return ControlFlow::Break(());
163 };
164 let Some(project) = project.upgrade() else {
165 return ControlFlow::Break(());
166 };
167 let this_pane = cx.entity().clone();
168 let item = if tab.pane == this_pane {
169 pane.item_for_index(tab.ix)
170 } else {
171 tab.pane.read(cx).item_for_index(tab.ix)
172 };
173 let Some(item) = item.filter(|item| item.downcast::<SubView>().is_some()) else {
174 return ControlFlow::Break(());
175 };
176
177 let source = tab.pane.clone();
178 let item_id_to_move = item.item_id();
179
180 let Ok(new_split_pane) = pane
181 .drag_split_direction()
182 .map(|split_direction| {
183 weak_running.update(cx, |running, cx| {
184 let new_pane =
185 new_debugger_pane(workspace.clone(), project.clone(), window, cx);
186 let _previous_subscription = running.pane_close_subscriptions.insert(
187 new_pane.entity_id(),
188 cx.subscribe(&new_pane, RunningState::handle_pane_event),
189 );
190 debug_assert!(_previous_subscription.is_none());
191 running
192 .panes
193 .split(&this_pane, &new_pane, split_direction)?;
194 anyhow::Ok(new_pane)
195 })
196 })
197 .transpose()
198 else {
199 return ControlFlow::Break(());
200 };
201
202 match new_split_pane.transpose() {
203 // Source pane may be the one currently updated, so defer the move.
204 Ok(Some(new_pane)) => cx
205 .spawn_in(window, async move |_, cx| {
206 cx.update(|window, cx| {
207 move_item(
208 &source,
209 &new_pane,
210 item_id_to_move,
211 new_pane.read(cx).active_item_index(),
212 window,
213 cx,
214 );
215 })
216 .ok();
217 })
218 .detach(),
219 // If we drop into existing pane or current pane,
220 // regular pane drop handler will take care of it,
221 // using the right tab index for the operation.
222 Ok(None) => return ControlFlow::Continue(()),
223 err @ Err(_) => {
224 err.log_err();
225 return ControlFlow::Break(());
226 }
227 };
228
229 ControlFlow::Break(())
230 }
231 };
232
233 let ret = cx.new(move |cx| {
234 let mut pane = Pane::new(
235 workspace.clone(),
236 project.clone(),
237 Default::default(),
238 None,
239 NoAction.boxed_clone(),
240 window,
241 cx,
242 );
243 pane.set_can_split(Some(Arc::new(move |pane, dragged_item, _window, cx| {
244 if let Some(tab) = dragged_item.downcast_ref::<DraggedTab>() {
245 let is_current_pane = tab.pane == cx.entity();
246 let Some(can_drag_away) = weak_running
247 .update(cx, |running_state, _| {
248 let current_panes = running_state.panes.panes();
249 !current_panes.contains(&&tab.pane)
250 || current_panes.len() > 1
251 || (!is_current_pane || pane.items_len() > 1)
252 })
253 .ok()
254 else {
255 return false;
256 };
257 if can_drag_away {
258 let item = if is_current_pane {
259 pane.item_for_index(tab.ix)
260 } else {
261 tab.pane.read(cx).item_for_index(tab.ix)
262 };
263 if let Some(item) = item {
264 return item.downcast::<SubView>().is_some();
265 }
266 }
267 }
268 false
269 })));
270 pane.display_nav_history_buttons(None);
271 pane.set_custom_drop_handle(cx, custom_drop_handle);
272 pane.set_should_display_tab_bar(|_, _| true);
273 pane.set_render_tab_bar_buttons(cx, |_, _, _| (None, None));
274 pane.set_render_tab_bar(cx, |pane, window, cx| {
275 let active_pane_item = pane.active_item();
276 h_flex()
277 .w_full()
278 .px_2()
279 .gap_1()
280 .h(Tab::container_height(cx))
281 .drag_over::<DraggedTab>(|bar, _, _, cx| {
282 bar.bg(cx.theme().colors().drop_target_background)
283 })
284 .on_drop(
285 cx.listener(move |this, dragged_tab: &DraggedTab, window, cx| {
286 this.drag_split_direction = None;
287 this.handle_tab_drop(dragged_tab, this.items_len(), window, cx)
288 }),
289 )
290 .bg(cx.theme().colors().tab_bar_background)
291 .border_b_1()
292 .border_color(cx.theme().colors().border)
293 .children(pane.items().enumerate().map(|(ix, item)| {
294 let selected = active_pane_item
295 .as_ref()
296 .map_or(false, |active| active.item_id() == item.item_id());
297 let item_ = item.boxed_clone();
298 div()
299 .id(SharedString::from(format!(
300 "debugger_tab_{}",
301 item.item_id().as_u64()
302 )))
303 .p_1()
304 .rounded_md()
305 .cursor_pointer()
306 .map(|this| {
307 if selected {
308 this.bg(cx.theme().colors().tab_active_background)
309 } else {
310 let hover_color = cx.theme().colors().element_hover;
311 this.hover(|style| style.bg(hover_color))
312 }
313 })
314 .on_click(cx.listener(move |this, _, window, cx| {
315 let index = this.index_for_item(&*item_);
316 if let Some(index) = index {
317 this.activate_item(index, true, true, window, cx);
318 }
319 }))
320 .child(item.tab_content(
321 TabContentParams {
322 selected,
323 ..Default::default()
324 },
325 window,
326 cx,
327 ))
328 .on_drop(
329 cx.listener(move |this, dragged_tab: &DraggedTab, window, cx| {
330 this.drag_split_direction = None;
331 this.handle_tab_drop(dragged_tab, ix, window, cx)
332 }),
333 )
334 .on_drag(
335 DraggedTab {
336 item: item.boxed_clone(),
337 pane: cx.entity().clone(),
338 detail: 0,
339 is_active: selected,
340 ix,
341 },
342 |tab, _, _, cx| cx.new(|_| tab.clone()),
343 )
344 }))
345 .into_any_element()
346 });
347 pane
348 });
349
350 ret
351}
352impl RunningState {
353 pub fn new(
354 session: Entity<Session>,
355 project: Entity<Project>,
356 workspace: WeakEntity<Workspace>,
357 window: &mut Window,
358 cx: &mut Context<Self>,
359 ) -> Self {
360 let focus_handle = cx.focus_handle();
361 let session_id = session.read(cx).session_id();
362 let weak_state = cx.weak_entity();
363 let stack_frame_list = cx.new(|cx| {
364 StackFrameList::new(workspace.clone(), session.clone(), weak_state, window, cx)
365 });
366
367 let variable_list =
368 cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx));
369
370 let module_list = cx.new(|cx| ModuleList::new(session.clone(), workspace.clone(), cx));
371
372 #[expect(unused)]
373 let loaded_source_list = cx.new(|cx| LoadedSourceList::new(session.clone(), cx));
374
375 let console = cx.new(|cx| {
376 Console::new(
377 session.clone(),
378 stack_frame_list.clone(),
379 variable_list.clone(),
380 window,
381 cx,
382 )
383 });
384
385 let _subscriptions = vec![
386 cx.observe(&module_list, |_, _, cx| cx.notify()),
387 cx.subscribe_in(&session, window, |this, _, event, window, cx| {
388 match event {
389 SessionEvent::Stopped(thread_id) => {
390 this.workspace
391 .update(cx, |workspace, cx| {
392 workspace.open_panel::<crate::DebugPanel>(window, cx);
393 })
394 .log_err();
395
396 if let Some(thread_id) = thread_id {
397 this.select_thread(*thread_id, cx);
398 }
399 }
400 SessionEvent::Threads => {
401 let threads = this.session.update(cx, |this, cx| this.threads(cx));
402 this.select_current_thread(&threads, cx);
403 }
404 _ => {}
405 }
406 cx.notify()
407 }),
408 ];
409
410 let leftmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
411 leftmost_pane.update(cx, |this, cx| {
412 this.add_item(
413 Box::new(SubView::new(
414 this.focus_handle(cx),
415 stack_frame_list.clone().into(),
416 SharedString::new_static("Frames"),
417 None,
418 cx,
419 )),
420 true,
421 false,
422 None,
423 window,
424 cx,
425 );
426 let breakpoints = BreakpointList::new(session.clone(), workspace.clone(), &project, cx);
427 this.add_item(
428 Box::new(SubView::new(
429 breakpoints.focus_handle(cx),
430 breakpoints.into(),
431 SharedString::new_static("Breakpoints"),
432 None,
433 cx,
434 )),
435 true,
436 false,
437 None,
438 window,
439 cx,
440 );
441 this.activate_item(0, false, false, window, cx);
442 });
443 let center_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
444 center_pane.update(cx, |this, cx| {
445 this.add_item(
446 Box::new(SubView::new(
447 variable_list.focus_handle(cx),
448 variable_list.clone().into(),
449 SharedString::new_static("Variables"),
450 None,
451 cx,
452 )),
453 true,
454 false,
455 None,
456 window,
457 cx,
458 );
459 this.add_item(
460 Box::new(SubView::new(
461 this.focus_handle(cx),
462 module_list.clone().into(),
463 SharedString::new_static("Modules"),
464 None,
465 cx,
466 )),
467 false,
468 false,
469 None,
470 window,
471 cx,
472 );
473 this.activate_item(0, false, false, window, cx);
474 });
475 let rightmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
476 rightmost_pane.update(cx, |this, cx| {
477 let weak_console = console.downgrade();
478 this.add_item(
479 Box::new(SubView::new(
480 this.focus_handle(cx),
481 console.clone().into(),
482 SharedString::new_static("Console"),
483 Some(Box::new(move |cx| {
484 weak_console
485 .read_with(cx, |console, cx| console.show_indicator(cx))
486 .unwrap_or_default()
487 })),
488 cx,
489 )),
490 true,
491 false,
492 None,
493 window,
494 cx,
495 );
496 });
497 let pane_close_subscriptions = HashMap::from_iter(
498 [&leftmost_pane, ¢er_pane, &rightmost_pane]
499 .into_iter()
500 .map(|entity| {
501 (
502 entity.entity_id(),
503 cx.subscribe(entity, Self::handle_pane_event),
504 )
505 }),
506 );
507 let group_root = workspace::PaneAxis::new(
508 gpui::Axis::Horizontal,
509 [leftmost_pane, center_pane, rightmost_pane]
510 .into_iter()
511 .map(workspace::Member::Pane)
512 .collect(),
513 );
514
515 let panes = PaneGroup::with_root(workspace::Member::Axis(group_root));
516
517 Self {
518 session,
519 workspace,
520 focus_handle,
521 variable_list,
522 _subscriptions,
523 thread_id: None,
524 _remote_id: None,
525 stack_frame_list,
526 session_id,
527 panes,
528 _module_list: module_list,
529 _console: console,
530 pane_close_subscriptions,
531 }
532 }
533
534 fn handle_pane_event(
535 this: &mut RunningState,
536 source_pane: Entity<Pane>,
537 event: &Event,
538 cx: &mut Context<RunningState>,
539 ) {
540 if let Event::Remove { .. } = event {
541 let _did_find_pane = this.panes.remove(&source_pane).is_ok();
542 debug_assert!(_did_find_pane);
543 cx.notify();
544 }
545 }
546 pub(crate) fn go_to_selected_stack_frame(&self, window: &Window, cx: &mut Context<Self>) {
547 if self.thread_id.is_some() {
548 self.stack_frame_list
549 .update(cx, |list, cx| list.go_to_selected_stack_frame(window, cx));
550 }
551 }
552
553 pub fn session(&self) -> &Entity<Session> {
554 &self.session
555 }
556
557 pub fn session_id(&self) -> SessionId {
558 self.session_id
559 }
560
561 pub(crate) fn selected_stack_frame_id(&self, cx: &App) -> Option<dap::StackFrameId> {
562 self.stack_frame_list.read(cx).selected_stack_frame_id()
563 }
564
565 #[cfg(test)]
566 pub fn stack_frame_list(&self) -> &Entity<StackFrameList> {
567 &self.stack_frame_list
568 }
569
570 #[cfg(test)]
571 pub fn console(&self) -> &Entity<Console> {
572 &self._console
573 }
574
575 #[cfg(test)]
576 pub(crate) fn module_list(&self) -> &Entity<ModuleList> {
577 &self._module_list
578 }
579
580 #[cfg(test)]
581 pub(crate) fn activate_modules_list(&self, window: &mut Window, cx: &mut App) {
582 let (variable_list_position, pane) = self
583 .panes
584 .panes()
585 .into_iter()
586 .find_map(|pane| {
587 pane.read(cx)
588 .items_of_type::<SubView>()
589 .position(|view| view.read(cx).tab_name == *"Modules")
590 .map(|view| (view, pane))
591 })
592 .unwrap();
593 pane.update(cx, |this, cx| {
594 this.activate_item(variable_list_position, true, true, window, cx);
595 })
596 }
597 #[cfg(test)]
598 pub(crate) fn variable_list(&self) -> &Entity<VariableList> {
599 &self.variable_list
600 }
601
602 pub fn capabilities(&self, cx: &App) -> Capabilities {
603 self.session().read(cx).capabilities().clone()
604 }
605
606 pub fn select_current_thread(
607 &mut self,
608 threads: &Vec<(Thread, ThreadStatus)>,
609 cx: &mut Context<Self>,
610 ) {
611 let selected_thread = self
612 .thread_id
613 .and_then(|thread_id| threads.iter().find(|(thread, _)| thread.id == thread_id.0))
614 .or_else(|| threads.first());
615
616 let Some((selected_thread, _)) = selected_thread else {
617 return;
618 };
619
620 if Some(ThreadId(selected_thread.id)) != self.thread_id {
621 self.select_thread(ThreadId(selected_thread.id), cx);
622 }
623 }
624
625 pub(crate) fn selected_thread_id(&self) -> Option<ThreadId> {
626 self.thread_id
627 }
628
629 pub fn thread_status(&self, cx: &App) -> Option<ThreadStatus> {
630 self.thread_id
631 .map(|id| self.session().read(cx).thread_status(id))
632 }
633
634 fn select_thread(&mut self, thread_id: ThreadId, cx: &mut Context<Self>) {
635 if self.thread_id.is_some_and(|id| id == thread_id) {
636 return;
637 }
638
639 self.thread_id = Some(thread_id);
640
641 self.stack_frame_list
642 .update(cx, |list, cx| list.refresh(cx));
643 cx.notify();
644 }
645
646 pub fn continue_thread(&mut self, cx: &mut Context<Self>) {
647 let Some(thread_id) = self.thread_id else {
648 return;
649 };
650
651 self.session().update(cx, |state, cx| {
652 state.continue_thread(thread_id, cx);
653 });
654 }
655
656 pub fn step_over(&mut self, cx: &mut Context<Self>) {
657 let Some(thread_id) = self.thread_id else {
658 return;
659 };
660
661 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
662
663 self.session().update(cx, |state, cx| {
664 state.step_over(thread_id, granularity, cx);
665 });
666 }
667
668 pub(crate) fn step_in(&mut self, cx: &mut Context<Self>) {
669 let Some(thread_id) = self.thread_id else {
670 return;
671 };
672
673 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
674
675 self.session().update(cx, |state, cx| {
676 state.step_in(thread_id, granularity, cx);
677 });
678 }
679
680 pub(crate) fn step_out(&mut self, cx: &mut Context<Self>) {
681 let Some(thread_id) = self.thread_id else {
682 return;
683 };
684
685 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
686
687 self.session().update(cx, |state, cx| {
688 state.step_out(thread_id, granularity, cx);
689 });
690 }
691
692 pub(crate) fn step_back(&mut self, cx: &mut Context<Self>) {
693 let Some(thread_id) = self.thread_id else {
694 return;
695 };
696
697 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
698
699 self.session().update(cx, |state, cx| {
700 state.step_back(thread_id, granularity, cx);
701 });
702 }
703
704 pub fn restart_session(&self, cx: &mut Context<Self>) {
705 self.session().update(cx, |state, cx| {
706 state.restart(None, cx);
707 });
708 }
709
710 pub fn pause_thread(&self, cx: &mut Context<Self>) {
711 let Some(thread_id) = self.thread_id else {
712 return;
713 };
714
715 self.session().update(cx, |state, cx| {
716 state.pause_thread(thread_id, cx);
717 });
718 }
719
720 pub(crate) fn shutdown(&mut self, cx: &mut Context<Self>) {
721 self.workspace
722 .update(cx, |workspace, cx| {
723 workspace
724 .project()
725 .read(cx)
726 .breakpoint_store()
727 .update(cx, |store, cx| {
728 store.remove_active_position(Some(self.session_id), cx)
729 })
730 })
731 .log_err();
732
733 self.session.update(cx, |session, cx| {
734 session.shutdown(cx).detach();
735 })
736 }
737
738 pub fn stop_thread(&self, cx: &mut Context<Self>) {
739 let Some(thread_id) = self.thread_id else {
740 return;
741 };
742
743 self.workspace
744 .update(cx, |workspace, cx| {
745 workspace
746 .project()
747 .read(cx)
748 .breakpoint_store()
749 .update(cx, |store, cx| {
750 store.remove_active_position(Some(self.session_id), cx)
751 })
752 })
753 .log_err();
754
755 self.session().update(cx, |state, cx| {
756 state.terminate_threads(Some(vec![thread_id; 1]), cx);
757 });
758 }
759
760 #[expect(
761 unused,
762 reason = "Support for disconnecting a client is not wired through yet"
763 )]
764 pub fn disconnect_client(&self, cx: &mut Context<Self>) {
765 self.session().update(cx, |state, cx| {
766 state.disconnect_client(cx);
767 });
768 }
769
770 pub fn toggle_ignore_breakpoints(&mut self, cx: &mut Context<Self>) {
771 self.session.update(cx, |session, cx| {
772 session.toggle_ignore_breakpoints(cx).detach();
773 });
774 }
775
776 pub(crate) fn thread_dropdown(
777 &self,
778 window: &mut Window,
779 cx: &mut Context<'_, RunningState>,
780 ) -> DropdownMenu {
781 let state = cx.entity();
782 let threads = self.session.update(cx, |this, cx| this.threads(cx));
783 let selected_thread_name = threads
784 .iter()
785 .find(|(thread, _)| self.thread_id.map(|id| id.0) == Some(thread.id))
786 .map(|(thread, _)| thread.name.clone())
787 .unwrap_or("Threads".to_owned());
788 DropdownMenu::new(
789 ("thread-list", self.session_id.0),
790 selected_thread_name,
791 ContextMenu::build(window, cx, move |mut this, _, _| {
792 for (thread, _) in threads {
793 let state = state.clone();
794 let thread_id = thread.id;
795 this = this.entry(thread.name, None, move |_, cx| {
796 state.update(cx, |state, cx| {
797 state.select_thread(ThreadId(thread_id), cx);
798 });
799 });
800 }
801 this
802 }),
803 )
804 }
805}
806
807impl EventEmitter<DebugPanelItemEvent> for RunningState {}
808
809impl Focusable for RunningState {
810 fn focus_handle(&self, _: &App) -> FocusHandle {
811 self.focus_handle.clone()
812 }
813}