use std::time::Duration; use collections::HashMap;use gpui::{Animation, AnimationExt as _, Entity, Transformation, percentage};use project::debugger::session::{ThreadId, ThreadStatus};use ui::{ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*}; use crate::{ debugger_panel::DebugPanel, session::{DebugSession, running::RunningState},}; impl DebugPanel { fn dropdown_label(label: impl Into<SharedString>) -> Label { Label::new(label).size(LabelSize::Small) } pub fn render_session_menu( &mut self, active_session: Option<Entity<DebugSession>>, running_state: Option<Entity<RunningState>>, window: &mut Window, cx: &mut Context<Self>, ) -> Option<impl IntoElement> { if let Some(running_state) = running_state { let sessions = self.sessions().clone(); let weak = cx.weak_entity(); let running_state = running_state.read(cx); let label = if let Some(active_session) = active_session.clone() { active_session.read(cx).session(cx).read(cx).label() } else { SharedString::new_static("Unknown Session") }; let is_terminated = running_state.session().read(cx).is_terminated(); let is_started = active_session .is_some_and(|session| session.read(cx).session(cx).read(cx).is_started()); let session_state_indicator = if is_terminated { Indicator::dot().color(Color::Error).into_any_element() } else if !is_started { Icon::new(IconName::ArrowCircle) .size(IconSize::Small) .color(Color::Muted) .with_animation( "arrow-circle", Animation::new(Duration::from_secs(2)).repeat(), |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), ) .into_any_element() } else { match running_state.thread_status(cx).unwrap_or_default() { ThreadStatus::Stopped => { Indicator::dot().color(Color::Conflict).into_any_element() } _ => Indicator::dot().color(Color::Success).into_any_element(), } }; let trigger = h_flex() .gap_2() .child(session_state_indicator) .justify_between() .child( DebugPanel::dropdown_label(label) .when(is_terminated, |this| this.strikethrough()), ) .into_any_element(); Some( DropdownMenu::new_with_element( "debugger-session-list", trigger, ContextMenu::build(window, cx, move |mut this, _, cx| { let context_menu = cx.weak_entity(); let mut session_depths = HashMap::default(); for session in sessions.into_iter() { let weak_session = session.downgrade(); let weak_session_id = weak_session.entity_id(); let session_id = session.read(cx).session_id(cx); let parent_depth = session .read(cx) .session(cx) .read(cx) .parent_id(cx) .and_then(|parent_id| session_depths.get(&parent_id).cloned()); let self_depth = *session_depths.entry(session_id).or_insert_with(|| { parent_depth.map(|depth| depth + 1).unwrap_or(0usize) }); this = this.custom_entry( { let weak = weak.clone(); let context_menu = context_menu.clone(); move |_, cx| { weak_session .read_with(cx, |session, cx| { let context_menu = context_menu.clone(); let id: SharedString = format!("debug-session-{}", session_id.0) .into(); h_flex() .w_full() .group(id.clone()) .justify_between() .child(session.label_element(self_depth, cx)) .child( IconButton::new( "close-debug-session", IconName::Close, ) .visible_on_hover(id.clone()) .icon_size(IconSize::Small) .on_click({ let weak = weak.clone(); move |_, window, cx| { weak.update(cx, |panel, cx| { panel.close_session( weak_session_id, window, cx, ); }) .ok(); context_menu .update(cx, |this, cx| { this.cancel( &Default::default(), window, cx, ); }) .ok(); } }), ) .into_any_element() }) .unwrap_or_else(|_| div().into_any_element()) } }, { let weak = weak.clone(); move |window, cx| { weak.update(cx, |panel, cx| { panel.activate_session(session.clone(), window, cx); }) .ok(); } }, ); } this }), ) .style(DropdownStyle::Ghost) .handle(self.session_picker_menu_handle.clone()), ) } else { None } } pub(crate) fn render_thread_dropdown( &self, running_state: &Entity<RunningState>, threads: Vec<(dap::Thread, ThreadStatus)>, window: &mut Window, cx: &mut Context<Self>, ) -> Option<DropdownMenu> { let running_state = running_state.clone(); let running_state_read = running_state.read(cx); let thread_id = running_state_read.thread_id(); let session = running_state_read.session(); let session_id = session.read(cx).session_id(); let session_terminated = session.read(cx).is_terminated(); let selected_thread_name = threads .iter() .find(|(thread, _)| thread_id.map(|id| id.0) == Some(thread.id)) .map(|(thread, _)| { thread .name .is_empty() .then(|| format!("Tid: {}", thread.id)) .unwrap_or_else(|| thread.name.clone()) }); if let Some(selected_thread_name) = selected_thread_name { let trigger = DebugPanel::dropdown_label(selected_thread_name).into_any_element(); Some( DropdownMenu::new_with_element( ("thread-list", session_id.0), trigger, ContextMenu::build(window, cx, move |mut this, _, _| { for (thread, _) in threads { let running_state = running_state.clone(); let thread_id = thread.id; let entry_name = thread .name .is_empty() .then(|| format!("Tid: {}", thread.id)) .unwrap_or_else(|| thread.name); this = this.entry(entry_name, None, move |window, cx| { running_state.update(cx, |running_state, cx| { running_state.select_thread(ThreadId(thread_id), window, cx); }); }); } this }), ) .disabled(session_terminated) .style(DropdownStyle::Ghost) .handle(self.thread_picker_menu_handle.clone()), ) } else { None } }}use std::time::Duration;
use collections::HashMap;
use gpui::{Animation, AnimationExt as _, Entity, Transformation, percentage};
use project::debugger::session::{ThreadId, ThreadStatus};
use ui::{ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
use crate::{
debugger_panel::DebugPanel,
session::{DebugSession, running::RunningState},
};
impl DebugPanel {
fn dropdown_label(label: impl Into<SharedString>) -> Label {
Label::new(label).size(LabelSize::Small)
}
pub fn render_session_menu(
&mut self,
active_session: Option<Entity<DebugSession>>,
running_state: Option<Entity<RunningState>>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<impl IntoElement> {
if let Some(running_state) = running_state {
let sessions = self.sessions().clone();
let weak = cx.weak_entity();
let running_state = running_state.read(cx);
let label = if let Some(active_session) = active_session.clone() {
active_session.read(cx).session(cx).read(cx).label()
} else {
SharedString::new_static("Unknown Session")
};
let is_terminated = running_state.session().read(cx).is_terminated();
let is_started = active_session
.is_some_and(|session| session.read(cx).session(cx).read(cx).is_started());
let session_state_indicator = if is_terminated {
Indicator::dot().color(Color::Error).into_any_element()
} else if !is_started {
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.color(Color::Muted)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
)
.into_any_element()
} else {
match running_state.thread_status(cx).unwrap_or_default() {
ThreadStatus::Stopped => {
Indicator::dot().color(Color::Conflict).into_any_element()
}
_ => Indicator::dot().color(Color::Success).into_any_element(),
}
};
let trigger = h_flex()
.gap_2()
.child(session_state_indicator)
.justify_between()
.child(
DebugPanel::dropdown_label(label)
.when(is_terminated, |this| this.strikethrough()),
)
.into_any_element();
Some(
DropdownMenu::new_with_element(
"debugger-session-list",
trigger,
ContextMenu::build(window, cx, move |mut this, _, cx| {
let context_menu = cx.weak_entity();
let mut session_depths = HashMap::default();
for session in sessions.into_iter() {
let weak_session = session.downgrade();
let weak_session_id = weak_session.entity_id();
let session_id = session.read(cx).session_id(cx);
let parent_depth = session
.read(cx)
.session(cx)
.read(cx)
.parent_id(cx)
.and_then(|parent_id| session_depths.get(&parent_id).cloned());
let self_depth =
*session_depths.entry(session_id).or_insert_with(|| {
parent_depth.map(|depth| depth + 1).unwrap_or(0usize)
});
this = this.custom_entry(
{
let weak = weak.clone();
let context_menu = context_menu.clone();
move |_, cx| {
weak_session
.read_with(cx, |session, cx| {
let context_menu = context_menu.clone();
let id: SharedString =
format!("debug-session-{}", session_id.0)
.into();
h_flex()
.w_full()
.group(id.clone())
.justify_between()
.child(session.label_element(self_depth, cx))
.child(
IconButton::new(
"close-debug-session",
IconName::Close,
)
.visible_on_hover(id.clone())
.icon_size(IconSize::Small)
.on_click({
let weak = weak.clone();
move |_, window, cx| {
weak.update(cx, |panel, cx| {
panel.close_session(
weak_session_id,
window,
cx,
);
})
.ok();
context_menu
.update(cx, |this, cx| {
this.cancel(
&Default::default(),
window,
cx,
);
})
.ok();
}
}),
)
.into_any_element()
})
.unwrap_or_else(|_| div().into_any_element())
}
},
{
let weak = weak.clone();
move |window, cx| {
weak.update(cx, |panel, cx| {
panel.activate_session(session.clone(), window, cx);
})
.ok();
}
},
);
}
this
}),
)
.style(DropdownStyle::Ghost)
.handle(self.session_picker_menu_handle.clone()),
)
} else {
None
}
}
pub(crate) fn render_thread_dropdown(
&self,
running_state: &Entity<RunningState>,
threads: Vec<(dap::Thread, ThreadStatus)>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<DropdownMenu> {
let running_state = running_state.clone();
let running_state_read = running_state.read(cx);
let thread_id = running_state_read.thread_id();
let session = running_state_read.session();
let session_id = session.read(cx).session_id();
let session_terminated = session.read(cx).is_terminated();
let selected_thread_name = threads
.iter()
.find(|(thread, _)| thread_id.map(|id| id.0) == Some(thread.id))
.map(|(thread, _)| {
thread
.name
.is_empty()
.then(|| format!("Tid: {}", thread.id))
.unwrap_or_else(|| thread.name.clone())
});
if let Some(selected_thread_name) = selected_thread_name {
let trigger = DebugPanel::dropdown_label(selected_thread_name).into_any_element();
Some(
DropdownMenu::new_with_element(
("thread-list", session_id.0),
trigger,
ContextMenu::build(window, cx, move |mut this, _, _| {
for (thread, _) in threads {
let running_state = running_state.clone();
let thread_id = thread.id;
let entry_name = thread
.name
.is_empty()
.then(|| format!("Tid: {}", thread.id))
.unwrap_or_else(|| thread.name);
this = this.entry(entry_name, None, move |window, cx| {
running_state.update(cx, |running_state, cx| {
running_state.select_thread(ThreadId(thread_id), window, cx);
});
});
}
this
}),
)
.disabled(session_terminated)
.style(DropdownStyle::Ghost)
.handle(self.thread_picker_menu_handle.clone()),
)
} else {
None
}
}
}