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