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 .h(Tab::container_height(cx))
279 .drag_over::<DraggedTab>(|bar, _, _, cx| {
280 bar.bg(cx.theme().colors().drop_target_background)
281 })
282 .on_drop(
283 cx.listener(move |this, dragged_tab: &DraggedTab, window, cx| {
284 this.drag_split_direction = None;
285 this.handle_tab_drop(dragged_tab, this.items_len(), window, cx)
286 }),
287 )
288 .bg(cx.theme().colors().tab_bar_background)
289 .border_b_1()
290 .border_color(cx.theme().colors().border)
291 .children(pane.items().enumerate().map(|(ix, item)| {
292 let selected = active_pane_item
293 .as_ref()
294 .map_or(false, |active| active.item_id() == item.item_id());
295 let item_ = item.boxed_clone();
296 div()
297 .id(SharedString::from(format!(
298 "debugger_tab_{}",
299 item.item_id().as_u64()
300 )))
301 .p_1()
302 .rounded_md()
303 .cursor_pointer()
304 .map(|this| {
305 if selected {
306 this.bg(cx.theme().colors().tab_active_background)
307 } else {
308 let hover_color = cx.theme().colors().element_hover;
309 this.hover(|style| style.bg(hover_color))
310 }
311 })
312 .on_click(cx.listener(move |this, _, window, cx| {
313 let index = this.index_for_item(&*item_);
314 if let Some(index) = index {
315 this.activate_item(index, true, true, window, cx);
316 }
317 }))
318 .child(item.tab_content(
319 TabContentParams {
320 selected,
321 ..Default::default()
322 },
323 window,
324 cx,
325 ))
326 .on_drop(
327 cx.listener(move |this, dragged_tab: &DraggedTab, window, cx| {
328 this.drag_split_direction = None;
329 this.handle_tab_drop(dragged_tab, ix, window, cx)
330 }),
331 )
332 .on_drag(
333 DraggedTab {
334 item: item.boxed_clone(),
335 pane: cx.entity().clone(),
336 detail: 0,
337 is_active: selected,
338 ix,
339 },
340 |tab, _, _, cx| cx.new(|_| tab.clone()),
341 )
342 }))
343 .into_any_element()
344 });
345 pane
346 });
347
348 ret
349}
350impl RunningState {
351 pub fn new(
352 session: Entity<Session>,
353 project: Entity<Project>,
354 workspace: WeakEntity<Workspace>,
355 window: &mut Window,
356 cx: &mut Context<Self>,
357 ) -> Self {
358 let focus_handle = cx.focus_handle();
359 let session_id = session.read(cx).session_id();
360 let weak_state = cx.weak_entity();
361 let stack_frame_list = cx.new(|cx| {
362 StackFrameList::new(workspace.clone(), session.clone(), weak_state, window, cx)
363 });
364
365 let variable_list =
366 cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx));
367
368 let module_list = cx.new(|cx| ModuleList::new(session.clone(), workspace.clone(), cx));
369
370 #[expect(unused)]
371 let loaded_source_list = cx.new(|cx| LoadedSourceList::new(session.clone(), cx));
372
373 let console = cx.new(|cx| {
374 Console::new(
375 session.clone(),
376 stack_frame_list.clone(),
377 variable_list.clone(),
378 window,
379 cx,
380 )
381 });
382
383 let _subscriptions = vec![
384 cx.observe(&module_list, |_, _, cx| cx.notify()),
385 cx.subscribe_in(&session, window, |this, _, event, window, cx| {
386 match event {
387 SessionEvent::Stopped(thread_id) => {
388 this.workspace
389 .update(cx, |workspace, cx| {
390 workspace.open_panel::<crate::DebugPanel>(window, cx);
391 })
392 .log_err();
393
394 if let Some(thread_id) = thread_id {
395 this.select_thread(*thread_id, cx);
396 }
397 }
398 SessionEvent::Threads => {
399 let threads = this.session.update(cx, |this, cx| this.threads(cx));
400 this.select_current_thread(&threads, cx);
401 }
402 _ => {}
403 }
404 cx.notify()
405 }),
406 ];
407
408 let leftmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
409 leftmost_pane.update(cx, |this, cx| {
410 this.add_item(
411 Box::new(SubView::new(
412 this.focus_handle(cx),
413 stack_frame_list.clone().into(),
414 SharedString::new_static("Frames"),
415 None,
416 cx,
417 )),
418 true,
419 false,
420 None,
421 window,
422 cx,
423 );
424 let breakpoints = BreakpointList::new(session.clone(), workspace.clone(), &project, cx);
425 this.add_item(
426 Box::new(SubView::new(
427 breakpoints.focus_handle(cx),
428 breakpoints.into(),
429 SharedString::new_static("Breakpoints"),
430 None,
431 cx,
432 )),
433 true,
434 false,
435 None,
436 window,
437 cx,
438 );
439 this.activate_item(0, false, false, window, cx);
440 });
441 let center_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
442 center_pane.update(cx, |this, cx| {
443 this.add_item(
444 Box::new(SubView::new(
445 variable_list.focus_handle(cx),
446 variable_list.clone().into(),
447 SharedString::new_static("Variables"),
448 None,
449 cx,
450 )),
451 true,
452 false,
453 None,
454 window,
455 cx,
456 );
457 this.add_item(
458 Box::new(SubView::new(
459 this.focus_handle(cx),
460 module_list.clone().into(),
461 SharedString::new_static("Modules"),
462 None,
463 cx,
464 )),
465 false,
466 false,
467 None,
468 window,
469 cx,
470 );
471 this.activate_item(0, false, false, window, cx);
472 });
473 let rightmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
474 rightmost_pane.update(cx, |this, cx| {
475 let weak_console = console.downgrade();
476 this.add_item(
477 Box::new(SubView::new(
478 this.focus_handle(cx),
479 console.clone().into(),
480 SharedString::new_static("Console"),
481 Some(Box::new(move |cx| {
482 weak_console
483 .read_with(cx, |console, cx| console.show_indicator(cx))
484 .unwrap_or_default()
485 })),
486 cx,
487 )),
488 true,
489 false,
490 None,
491 window,
492 cx,
493 );
494 });
495 let pane_close_subscriptions = HashMap::from_iter(
496 [&leftmost_pane, ¢er_pane, &rightmost_pane]
497 .into_iter()
498 .map(|entity| {
499 (
500 entity.entity_id(),
501 cx.subscribe(entity, Self::handle_pane_event),
502 )
503 }),
504 );
505 let group_root = workspace::PaneAxis::new(
506 gpui::Axis::Horizontal,
507 [leftmost_pane, center_pane, rightmost_pane]
508 .into_iter()
509 .map(workspace::Member::Pane)
510 .collect(),
511 );
512
513 let panes = PaneGroup::with_root(workspace::Member::Axis(group_root));
514
515 Self {
516 session,
517 workspace,
518 focus_handle,
519 variable_list,
520 _subscriptions,
521 thread_id: None,
522 _remote_id: None,
523 stack_frame_list,
524 session_id,
525 panes,
526 _module_list: module_list,
527 _console: console,
528 pane_close_subscriptions,
529 }
530 }
531
532 fn handle_pane_event(
533 this: &mut RunningState,
534 source_pane: Entity<Pane>,
535 event: &Event,
536 cx: &mut Context<RunningState>,
537 ) {
538 if let Event::Remove { .. } = event {
539 let _did_find_pane = this.panes.remove(&source_pane).is_ok();
540 debug_assert!(_did_find_pane);
541 cx.notify();
542 }
543 }
544 pub(crate) fn go_to_selected_stack_frame(&self, window: &Window, cx: &mut Context<Self>) {
545 if self.thread_id.is_some() {
546 self.stack_frame_list
547 .update(cx, |list, cx| list.go_to_selected_stack_frame(window, cx));
548 }
549 }
550
551 pub fn session(&self) -> &Entity<Session> {
552 &self.session
553 }
554
555 pub fn session_id(&self) -> SessionId {
556 self.session_id
557 }
558
559 pub(crate) fn selected_stack_frame_id(&self, cx: &App) -> Option<dap::StackFrameId> {
560 self.stack_frame_list.read(cx).selected_stack_frame_id()
561 }
562
563 #[cfg(test)]
564 pub fn stack_frame_list(&self) -> &Entity<StackFrameList> {
565 &self.stack_frame_list
566 }
567
568 #[cfg(test)]
569 pub fn console(&self) -> &Entity<Console> {
570 &self._console
571 }
572
573 #[cfg(test)]
574 pub(crate) fn module_list(&self) -> &Entity<ModuleList> {
575 &self._module_list
576 }
577
578 #[cfg(test)]
579 pub(crate) fn activate_modules_list(&self, window: &mut Window, cx: &mut App) {
580 let (variable_list_position, pane) = self
581 .panes
582 .panes()
583 .into_iter()
584 .find_map(|pane| {
585 pane.read(cx)
586 .items_of_type::<SubView>()
587 .position(|view| view.read(cx).tab_name == *"Modules")
588 .map(|view| (view, pane))
589 })
590 .unwrap();
591 pane.update(cx, |this, cx| {
592 this.activate_item(variable_list_position, true, true, window, cx);
593 })
594 }
595 #[cfg(test)]
596 pub(crate) fn variable_list(&self) -> &Entity<VariableList> {
597 &self.variable_list
598 }
599
600 pub fn capabilities(&self, cx: &App) -> Capabilities {
601 self.session().read(cx).capabilities().clone()
602 }
603
604 pub fn select_current_thread(
605 &mut self,
606 threads: &Vec<(Thread, ThreadStatus)>,
607 cx: &mut Context<Self>,
608 ) {
609 let selected_thread = self
610 .thread_id
611 .and_then(|thread_id| threads.iter().find(|(thread, _)| thread.id == thread_id.0))
612 .or_else(|| threads.first());
613
614 let Some((selected_thread, _)) = selected_thread else {
615 return;
616 };
617
618 if Some(ThreadId(selected_thread.id)) != self.thread_id {
619 self.select_thread(ThreadId(selected_thread.id), cx);
620 }
621 }
622
623 pub(crate) fn selected_thread_id(&self) -> Option<ThreadId> {
624 self.thread_id
625 }
626
627 pub fn thread_status(&self, cx: &App) -> Option<ThreadStatus> {
628 self.thread_id
629 .map(|id| self.session().read(cx).thread_status(id))
630 }
631
632 fn select_thread(&mut self, thread_id: ThreadId, cx: &mut Context<Self>) {
633 if self.thread_id.is_some_and(|id| id == thread_id) {
634 return;
635 }
636
637 self.thread_id = Some(thread_id);
638
639 self.stack_frame_list
640 .update(cx, |list, cx| list.refresh(cx));
641 cx.notify();
642 }
643
644 pub fn continue_thread(&mut self, cx: &mut Context<Self>) {
645 let Some(thread_id) = self.thread_id else {
646 return;
647 };
648
649 self.session().update(cx, |state, cx| {
650 state.continue_thread(thread_id, cx);
651 });
652 }
653
654 pub fn step_over(&mut self, cx: &mut Context<Self>) {
655 let Some(thread_id) = self.thread_id else {
656 return;
657 };
658
659 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
660
661 self.session().update(cx, |state, cx| {
662 state.step_over(thread_id, granularity, cx);
663 });
664 }
665
666 pub(crate) fn step_in(&mut self, cx: &mut Context<Self>) {
667 let Some(thread_id) = self.thread_id else {
668 return;
669 };
670
671 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
672
673 self.session().update(cx, |state, cx| {
674 state.step_in(thread_id, granularity, cx);
675 });
676 }
677
678 pub(crate) fn step_out(&mut self, cx: &mut Context<Self>) {
679 let Some(thread_id) = self.thread_id else {
680 return;
681 };
682
683 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
684
685 self.session().update(cx, |state, cx| {
686 state.step_out(thread_id, granularity, cx);
687 });
688 }
689
690 pub(crate) fn step_back(&mut self, cx: &mut Context<Self>) {
691 let Some(thread_id) = self.thread_id else {
692 return;
693 };
694
695 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
696
697 self.session().update(cx, |state, cx| {
698 state.step_back(thread_id, granularity, cx);
699 });
700 }
701
702 pub fn restart_session(&self, cx: &mut Context<Self>) {
703 self.session().update(cx, |state, cx| {
704 state.restart(None, cx);
705 });
706 }
707
708 pub fn pause_thread(&self, cx: &mut Context<Self>) {
709 let Some(thread_id) = self.thread_id else {
710 return;
711 };
712
713 self.session().update(cx, |state, cx| {
714 state.pause_thread(thread_id, cx);
715 });
716 }
717
718 pub(crate) fn shutdown(&mut self, cx: &mut Context<Self>) {
719 self.workspace
720 .update(cx, |workspace, cx| {
721 workspace
722 .project()
723 .read(cx)
724 .breakpoint_store()
725 .update(cx, |store, cx| {
726 store.remove_active_position(Some(self.session_id), cx)
727 })
728 })
729 .log_err();
730
731 self.session.update(cx, |session, cx| {
732 session.shutdown(cx).detach();
733 })
734 }
735
736 pub fn stop_thread(&self, cx: &mut Context<Self>) {
737 let Some(thread_id) = self.thread_id else {
738 return;
739 };
740
741 self.workspace
742 .update(cx, |workspace, cx| {
743 workspace
744 .project()
745 .read(cx)
746 .breakpoint_store()
747 .update(cx, |store, cx| {
748 store.remove_active_position(Some(self.session_id), cx)
749 })
750 })
751 .log_err();
752
753 self.session().update(cx, |state, cx| {
754 state.terminate_threads(Some(vec![thread_id; 1]), cx);
755 });
756 }
757
758 #[expect(
759 unused,
760 reason = "Support for disconnecting a client is not wired through yet"
761 )]
762 pub fn disconnect_client(&self, cx: &mut Context<Self>) {
763 self.session().update(cx, |state, cx| {
764 state.disconnect_client(cx);
765 });
766 }
767
768 pub fn toggle_ignore_breakpoints(&mut self, cx: &mut Context<Self>) {
769 self.session.update(cx, |session, cx| {
770 session.toggle_ignore_breakpoints(cx).detach();
771 });
772 }
773
774 pub(crate) fn thread_dropdown(
775 &self,
776 window: &mut Window,
777 cx: &mut Context<'_, RunningState>,
778 ) -> DropdownMenu {
779 let state = cx.entity();
780 let threads = self.session.update(cx, |this, cx| this.threads(cx));
781 let selected_thread_name = threads
782 .iter()
783 .find(|(thread, _)| self.thread_id.map(|id| id.0) == Some(thread.id))
784 .map(|(thread, _)| thread.name.clone())
785 .unwrap_or("Threads".to_owned());
786 DropdownMenu::new(
787 ("thread-list", self.session_id.0),
788 selected_thread_name,
789 ContextMenu::build(window, cx, move |mut this, _, _| {
790 for (thread, _) in threads {
791 let state = state.clone();
792 let thread_id = thread.id;
793 this = this.entry(thread.name, None, move |_, cx| {
794 state.update(cx, |state, cx| {
795 state.select_thread(ThreadId(thread_id), cx);
796 });
797 });
798 }
799 this
800 }),
801 )
802 }
803}
804
805impl EventEmitter<DebugPanelItemEvent> for RunningState {}
806
807impl Focusable for RunningState {
808 fn focus_handle(&self, _: &App) -> FocusHandle {
809 self.focus_handle.clone()
810 }
811}