dropdown_menus.rs

  1use std::time::Duration;
  2
  3use collections::HashMap;
  4use gpui::{Animation, AnimationExt as _, Entity, Transformation, percentage};
  5use project::debugger::session::{ThreadId, ThreadStatus};
  6use ui::{ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
  7
  8use crate::{
  9    debugger_panel::DebugPanel,
 10    session::{DebugSession, running::RunningState},
 11};
 12
 13impl DebugPanel {
 14    fn dropdown_label(label: impl Into<SharedString>) -> Label {
 15        Label::new(label).size(LabelSize::Small)
 16    }
 17
 18    pub fn render_session_menu(
 19        &mut self,
 20        active_session: Option<Entity<DebugSession>>,
 21        running_state: Option<Entity<RunningState>>,
 22        window: &mut Window,
 23        cx: &mut Context<Self>,
 24    ) -> Option<impl IntoElement> {
 25        if let Some(running_state) = running_state {
 26            let sessions = self.sessions().clone();
 27            let weak = cx.weak_entity();
 28            let running_state = running_state.read(cx);
 29            let label = if let Some(active_session) = active_session.clone() {
 30                active_session.read(cx).session(cx).read(cx).label()
 31            } else {
 32                SharedString::new_static("Unknown Session")
 33            };
 34
 35            let is_terminated = running_state.session().read(cx).is_terminated();
 36            let is_started = active_session
 37                .is_some_and(|session| session.read(cx).session(cx).read(cx).is_started());
 38
 39            let session_state_indicator = if is_terminated {
 40                Indicator::dot().color(Color::Error).into_any_element()
 41            } else if !is_started {
 42                Icon::new(IconName::ArrowCircle)
 43                    .size(IconSize::Small)
 44                    .color(Color::Muted)
 45                    .with_animation(
 46                        "arrow-circle",
 47                        Animation::new(Duration::from_secs(2)).repeat(),
 48                        |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
 49                    )
 50                    .into_any_element()
 51            } else {
 52                match running_state.thread_status(cx).unwrap_or_default() {
 53                    ThreadStatus::Stopped => {
 54                        Indicator::dot().color(Color::Conflict).into_any_element()
 55                    }
 56                    _ => Indicator::dot().color(Color::Success).into_any_element(),
 57                }
 58            };
 59
 60            let trigger = h_flex()
 61                .gap_2()
 62                .child(session_state_indicator)
 63                .justify_between()
 64                .child(
 65                    DebugPanel::dropdown_label(label)
 66                        .when(is_terminated, |this| this.strikethrough()),
 67                )
 68                .into_any_element();
 69
 70            Some(
 71                DropdownMenu::new_with_element(
 72                    "debugger-session-list",
 73                    trigger,
 74                    ContextMenu::build(window, cx, move |mut this, _, cx| {
 75                        let context_menu = cx.weak_entity();
 76                        let mut session_depths = HashMap::default();
 77                        for session in sessions.into_iter() {
 78                            let weak_session = session.downgrade();
 79                            let weak_session_id = weak_session.entity_id();
 80                            let session_id = session.read(cx).session_id(cx);
 81                            let parent_depth = session
 82                                .read(cx)
 83                                .session(cx)
 84                                .read(cx)
 85                                .parent_id(cx)
 86                                .and_then(|parent_id| session_depths.get(&parent_id).cloned());
 87                            let self_depth =
 88                                *session_depths.entry(session_id).or_insert_with(|| {
 89                                    parent_depth.map(|depth| depth + 1).unwrap_or(0usize)
 90                                });
 91                            this = this.custom_entry(
 92                                {
 93                                    let weak = weak.clone();
 94                                    let context_menu = context_menu.clone();
 95                                    move |_, cx| {
 96                                        weak_session
 97                                            .read_with(cx, |session, cx| {
 98                                                let context_menu = context_menu.clone();
 99
100                                                let id: SharedString =
101                                                    format!("debug-session-{}", session_id.0)
102                                                        .into();
103
104                                                h_flex()
105                                                    .w_full()
106                                                    .group(id.clone())
107                                                    .justify_between()
108                                                    .child(session.label_element(self_depth, cx))
109                                                    .child(
110                                                        IconButton::new(
111                                                            "close-debug-session",
112                                                            IconName::Close,
113                                                        )
114                                                        .visible_on_hover(id.clone())
115                                                        .icon_size(IconSize::Small)
116                                                        .on_click({
117                                                            let weak = weak.clone();
118                                                            move |_, window, cx| {
119                                                                weak.update(cx, |panel, cx| {
120                                                                    panel.close_session(
121                                                                        weak_session_id,
122                                                                        window,
123                                                                        cx,
124                                                                    );
125                                                                })
126                                                                .ok();
127                                                                context_menu
128                                                                    .update(cx, |this, cx| {
129                                                                        this.cancel(
130                                                                            &Default::default(),
131                                                                            window,
132                                                                            cx,
133                                                                        );
134                                                                    })
135                                                                    .ok();
136                                                            }
137                                                        }),
138                                                    )
139                                                    .into_any_element()
140                                            })
141                                            .unwrap_or_else(|_| div().into_any_element())
142                                    }
143                                },
144                                {
145                                    let weak = weak.clone();
146                                    move |window, cx| {
147                                        weak.update(cx, |panel, cx| {
148                                            panel.activate_session(session.clone(), window, cx);
149                                        })
150                                        .ok();
151                                    }
152                                },
153                            );
154                        }
155                        this
156                    }),
157                )
158                .style(DropdownStyle::Ghost)
159                .handle(self.session_picker_menu_handle.clone()),
160            )
161        } else {
162            None
163        }
164    }
165
166    pub(crate) fn render_thread_dropdown(
167        &self,
168        running_state: &Entity<RunningState>,
169        threads: Vec<(dap::Thread, ThreadStatus)>,
170        window: &mut Window,
171        cx: &mut Context<Self>,
172    ) -> Option<DropdownMenu> {
173        let running_state = running_state.clone();
174        let running_state_read = running_state.read(cx);
175        let thread_id = running_state_read.thread_id();
176        let session = running_state_read.session();
177        let session_id = session.read(cx).session_id();
178        let session_terminated = session.read(cx).is_terminated();
179        let selected_thread_name = threads
180            .iter()
181            .find(|(thread, _)| thread_id.map(|id| id.0) == Some(thread.id))
182            .map(|(thread, _)| {
183                thread
184                    .name
185                    .is_empty()
186                    .then(|| format!("Tid: {}", thread.id))
187                    .unwrap_or_else(|| thread.name.clone())
188            });
189
190        if let Some(selected_thread_name) = selected_thread_name {
191            let trigger = DebugPanel::dropdown_label(selected_thread_name).into_any_element();
192            Some(
193                DropdownMenu::new_with_element(
194                    ("thread-list", session_id.0),
195                    trigger,
196                    ContextMenu::build(window, cx, move |mut this, _, _| {
197                        for (thread, _) in threads {
198                            let running_state = running_state.clone();
199                            let thread_id = thread.id;
200                            let entry_name = thread
201                                .name
202                                .is_empty()
203                                .then(|| format!("Tid: {}", thread.id))
204                                .unwrap_or_else(|| thread.name);
205
206                            this = this.entry(entry_name, None, move |window, cx| {
207                                running_state.update(cx, |running_state, cx| {
208                                    running_state.select_thread(ThreadId(thread_id), window, cx);
209                                });
210                            });
211                        }
212                        this
213                    }),
214                )
215                .disabled(session_terminated)
216                .style(DropdownStyle::Ghost)
217                .handle(self.thread_picker_menu_handle.clone()),
218            )
219        } else {
220            None
221        }
222    }
223}