dropdown_menus.rs

  1use std::{rc::Rc, time::Duration};
  2
  3use collections::HashMap;
  4use gpui::{Animation, AnimationExt as _, Entity, Transformation, WeakEntity, percentage};
  5use project::debugger::session::{ThreadId, ThreadStatus};
  6use ui::{ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
  7use util::{maybe, truncate_and_trailoff};
  8
  9use crate::{
 10    debugger_panel::DebugPanel,
 11    session::{DebugSession, running::RunningState},
 12};
 13
 14struct SessionListEntry {
 15    ancestors: Vec<Entity<DebugSession>>,
 16    leaf: Entity<DebugSession>,
 17}
 18
 19impl SessionListEntry {
 20    pub(crate) fn label_element(&self, depth: usize, cx: &mut App) -> AnyElement {
 21        const MAX_LABEL_CHARS: usize = 150;
 22
 23        let mut label = String::new();
 24        for ancestor in &self.ancestors {
 25            label.push_str(&ancestor.update(cx, |ancestor, cx| {
 26                ancestor.label(cx).unwrap_or("(child)".into())
 27            }));
 28            label.push_str(" ยป ");
 29        }
 30        label.push_str(
 31            &self
 32                .leaf
 33                .update(cx, |leaf, cx| leaf.label(cx).unwrap_or("(child)".into())),
 34        );
 35        let label = truncate_and_trailoff(&label, MAX_LABEL_CHARS);
 36
 37        let is_terminated = self
 38            .leaf
 39            .read(cx)
 40            .running_state
 41            .read(cx)
 42            .session()
 43            .read(cx)
 44            .is_terminated();
 45        let icon = {
 46            if is_terminated {
 47                Some(Indicator::dot().color(Color::Error))
 48            } else {
 49                match self
 50                    .leaf
 51                    .read(cx)
 52                    .running_state
 53                    .read(cx)
 54                    .thread_status(cx)
 55                    .unwrap_or_default()
 56                {
 57                    project::debugger::session::ThreadStatus::Stopped => {
 58                        Some(Indicator::dot().color(Color::Conflict))
 59                    }
 60                    _ => Some(Indicator::dot().color(Color::Success)),
 61                }
 62            }
 63        };
 64
 65        h_flex()
 66            .id("session-label")
 67            .ml(depth * px(16.0))
 68            .gap_2()
 69            .when_some(icon, |this, indicator| this.child(indicator))
 70            .justify_between()
 71            .child(
 72                Label::new(label)
 73                    .size(LabelSize::Small)
 74                    .when(is_terminated, |this| this.strikethrough()),
 75            )
 76            .into_any_element()
 77    }
 78}
 79
 80impl DebugPanel {
 81    fn dropdown_label(label: impl Into<SharedString>) -> Label {
 82        const MAX_LABEL_CHARS: usize = 50;
 83        let label = truncate_and_trailoff(&label.into(), MAX_LABEL_CHARS);
 84        Label::new(label).size(LabelSize::Small)
 85    }
 86
 87    pub fn render_session_menu(
 88        &mut self,
 89        active_session: Option<Entity<DebugSession>>,
 90        running_state: Option<Entity<RunningState>>,
 91        window: &mut Window,
 92        cx: &mut Context<Self>,
 93    ) -> Option<impl IntoElement> {
 94        let running_state = running_state?;
 95
 96        let mut session_entries = Vec::with_capacity(self.sessions_with_children.len() * 3);
 97        let mut sessions_with_children = self.sessions_with_children.iter().peekable();
 98
 99        while let Some((root, children)) = sessions_with_children.next() {
100            let root_entry = if let Ok([single_child]) = <&[_; 1]>::try_from(children.as_slice())
101                && let Some(single_child) = single_child.upgrade()
102                && single_child.read(cx).quirks.compact
103            {
104                sessions_with_children.next();
105                SessionListEntry {
106                    leaf: single_child.clone(),
107                    ancestors: vec![root.clone()],
108                }
109            } else {
110                SessionListEntry {
111                    leaf: root.clone(),
112                    ancestors: Vec::new(),
113                }
114            };
115            session_entries.push(root_entry);
116
117            session_entries.extend(
118                sessions_with_children
119                    .by_ref()
120                    .take_while(|(session, _)| {
121                        session
122                            .read(cx)
123                            .session(cx)
124                            .read(cx)
125                            .parent_id(cx)
126                            .is_some()
127                    })
128                    .map(|(session, _)| SessionListEntry {
129                        leaf: session.clone(),
130                        ancestors: vec![],
131                    }),
132            );
133        }
134
135        let weak = cx.weak_entity();
136        let trigger_label = if let Some(active_session) = active_session.clone() {
137            active_session.update(cx, |active_session, cx| {
138                active_session.label(cx).unwrap_or("(child)".into())
139            })
140        } else {
141            SharedString::new_static("Unknown Session")
142        };
143        let running_state = running_state.read(cx);
144
145        let is_terminated = running_state.session().read(cx).is_terminated();
146        let is_started = active_session
147            .is_some_and(|session| session.read(cx).session(cx).read(cx).is_started());
148
149        let session_state_indicator = if is_terminated {
150            Indicator::dot().color(Color::Error).into_any_element()
151        } else if !is_started {
152            Icon::new(IconName::ArrowCircle)
153                .size(IconSize::Small)
154                .color(Color::Muted)
155                .with_animation(
156                    "arrow-circle",
157                    Animation::new(Duration::from_secs(2)).repeat(),
158                    |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
159                )
160                .into_any_element()
161        } else {
162            match running_state.thread_status(cx).unwrap_or_default() {
163                ThreadStatus::Stopped => Indicator::dot().color(Color::Conflict).into_any_element(),
164                _ => Indicator::dot().color(Color::Success).into_any_element(),
165            }
166        };
167
168        let trigger = h_flex()
169            .gap_2()
170            .child(session_state_indicator)
171            .justify_between()
172            .child(
173                DebugPanel::dropdown_label(trigger_label)
174                    .when(is_terminated, |this| this.strikethrough()),
175            )
176            .into_any_element();
177
178        let menu = DropdownMenu::new_with_element(
179            "debugger-session-list",
180            trigger,
181            ContextMenu::build(window, cx, move |mut this, _, cx| {
182                let context_menu = cx.weak_entity();
183                let mut session_depths = HashMap::default();
184                for session_entry in session_entries {
185                    let session_id = session_entry.leaf.read(cx).session_id(cx);
186                    let parent_depth = session_entry
187                        .ancestors
188                        .first()
189                        .unwrap_or(&session_entry.leaf)
190                        .read(cx)
191                        .session(cx)
192                        .read(cx)
193                        .parent_id(cx)
194                        .and_then(|parent_id| session_depths.get(&parent_id).cloned());
195                    let self_depth = *session_depths
196                        .entry(session_id)
197                        .or_insert_with(|| parent_depth.map(|depth| depth + 1).unwrap_or(0usize));
198                    this = this.custom_entry(
199                        {
200                            let weak = weak.clone();
201                            let context_menu = context_menu.clone();
202                            let ancestors: Rc<[_]> = session_entry
203                                .ancestors
204                                .iter()
205                                .map(|session| session.downgrade())
206                                .collect();
207                            let leaf = session_entry.leaf.downgrade();
208                            move |window, cx| {
209                                Self::render_session_menu_entry(
210                                    weak.clone(),
211                                    context_menu.clone(),
212                                    ancestors.clone(),
213                                    leaf.clone(),
214                                    self_depth,
215                                    window,
216                                    cx,
217                                )
218                            }
219                        },
220                        {
221                            let weak = weak.clone();
222                            let leaf = session_entry.leaf.clone();
223                            move |window, cx| {
224                                weak.update(cx, |panel, cx| {
225                                    panel.activate_session(leaf.clone(), window, cx);
226                                })
227                                .ok();
228                            }
229                        },
230                    );
231                }
232                this
233            }),
234        )
235        .style(DropdownStyle::Ghost)
236        .handle(self.session_picker_menu_handle.clone());
237
238        Some(menu)
239    }
240
241    fn render_session_menu_entry(
242        weak: WeakEntity<DebugPanel>,
243        context_menu: WeakEntity<ContextMenu>,
244        ancestors: Rc<[WeakEntity<DebugSession>]>,
245        leaf: WeakEntity<DebugSession>,
246        self_depth: usize,
247        _window: &mut Window,
248        cx: &mut App,
249    ) -> AnyElement {
250        let Some(session_entry) = maybe!({
251            let ancestors = ancestors
252                .iter()
253                .map(|ancestor| ancestor.upgrade())
254                .collect::<Option<Vec<_>>>()?;
255            let leaf = leaf.upgrade()?;
256            Some(SessionListEntry { ancestors, leaf })
257        }) else {
258            return div().into_any_element();
259        };
260
261        let id: SharedString = format!(
262            "debug-session-{}",
263            session_entry.leaf.read(cx).session_id(cx).0
264        )
265        .into();
266        let session_entity_id = session_entry.leaf.entity_id();
267
268        h_flex()
269            .w_full()
270            .group(id.clone())
271            .justify_between()
272            .child(session_entry.label_element(self_depth, cx))
273            .child(
274                IconButton::new("close-debug-session", IconName::Close)
275                    .visible_on_hover(id)
276                    .icon_size(IconSize::Small)
277                    .on_click({
278                        move |_, window, cx| {
279                            weak.update(cx, |panel, cx| {
280                                panel.close_session(session_entity_id, window, cx);
281                            })
282                            .ok();
283                            context_menu
284                                .update(cx, |this, cx| {
285                                    this.cancel(&Default::default(), window, cx);
286                                })
287                                .ok();
288                        }
289                    }),
290            )
291            .into_any_element()
292    }
293
294    pub(crate) fn render_thread_dropdown(
295        &self,
296        running_state: &Entity<RunningState>,
297        threads: Vec<(dap::Thread, ThreadStatus)>,
298        window: &mut Window,
299        cx: &mut Context<Self>,
300    ) -> Option<DropdownMenu> {
301        const MAX_LABEL_CHARS: usize = 150;
302
303        let running_state = running_state.clone();
304        let running_state_read = running_state.read(cx);
305        let thread_id = running_state_read.thread_id();
306        let session = running_state_read.session();
307        let session_id = session.read(cx).session_id();
308        let session_terminated = session.read(cx).is_terminated();
309        let selected_thread_name = threads
310            .iter()
311            .find(|(thread, _)| thread_id.map(|id| id.0) == Some(thread.id))
312            .map(|(thread, _)| {
313                thread
314                    .name
315                    .is_empty()
316                    .then(|| format!("Tid: {}", thread.id))
317                    .unwrap_or_else(|| thread.name.clone())
318            });
319
320        if let Some(selected_thread_name) = selected_thread_name {
321            let trigger = DebugPanel::dropdown_label(selected_thread_name).into_any_element();
322            Some(
323                DropdownMenu::new_with_element(
324                    ("thread-list", session_id.0),
325                    trigger,
326                    ContextMenu::build(window, cx, move |mut this, _, _| {
327                        for (thread, _) in threads {
328                            let running_state = running_state.clone();
329                            let thread_id = thread.id;
330                            let entry_name = thread
331                                .name
332                                .is_empty()
333                                .then(|| format!("Tid: {}", thread.id))
334                                .unwrap_or_else(|| thread.name);
335                            let entry_name = truncate_and_trailoff(&entry_name, MAX_LABEL_CHARS);
336
337                            this = this.entry(entry_name, None, move |window, cx| {
338                                running_state.update(cx, |running_state, cx| {
339                                    running_state.select_thread(ThreadId(thread_id), window, cx);
340                                });
341                            });
342                        }
343                        this
344                    }),
345                )
346                .disabled(session_terminated)
347                .style(DropdownStyle::Ghost)
348                .handle(self.thread_picker_menu_handle.clone()),
349            )
350        } else {
351            None
352        }
353    }
354}