debugger_ui: Preview thread state when using the dropdown (#28778)

Cole Miller created

This PR changes the thread list dropdown menu in the debugger UI to
eagerly preview the state of a thread when selecting it, instead of
waiting until confirming the selection.

Release Notes:

- N/A

Change summary

crates/debugger_ui/src/session/running.rs |  2 
crates/language_tools/src/lsp_log.rs      | 82 +++++++++++++-----------
crates/ui/src/components/context_menu.rs  | 71 ++++++++++++++++++---
3 files changed, 106 insertions(+), 49 deletions(-)

Detailed changes

crates/debugger_ui/src/session/running.rs 🔗

@@ -768,7 +768,7 @@ impl RunningState {
         DropdownMenu::new(
             ("thread-list", self.session_id.0),
             selected_thread_name,
-            ContextMenu::build(window, cx, move |mut this, _, _| {
+            ContextMenu::build_eager(window, cx, move |mut this, _, _| {
                 for (thread, _) in threads {
                     let state = state.clone();
                     let thread_id = thread.id;

crates/language_tools/src/lsp_log.rs 🔗

@@ -1421,18 +1421,21 @@ impl Render for LspLogToolbarItemView {
                                                         })
                                                     })?;
 
-                                                ContextMenu::build(window, cx, |mut menu, _, _| {
-                                                    let log_view = log_view.clone();
-
-                                                    for (option, label) in [
-                                                        (TraceValue::Off, "Off"),
-                                                        (TraceValue::Messages, "Messages"),
-                                                        (TraceValue::Verbose, "Verbose"),
-                                                    ] {
-                                                        menu = menu.entry(label, None, {
-                                                            let log_view = log_view.clone();
-                                                            move |_, cx| {
-                                                                log_view.update(cx, |this, cx| {
+                                                ContextMenu::build(
+                                                    window,
+                                                    cx,
+                                                    |mut menu, window, cx| {
+                                                        let log_view = log_view.clone();
+
+                                                        for (option, label) in [
+                                                            (TraceValue::Off, "Off"),
+                                                            (TraceValue::Messages, "Messages"),
+                                                            (TraceValue::Verbose, "Verbose"),
+                                                        ] {
+                                                            menu = menu.entry(label, None, {
+                                                                let log_view = log_view.clone();
+                                                                move |_, cx| {
+                                                                    log_view.update(cx, |this, cx| {
                                                                     if let Some(id) =
                                                                         this.current_server_id
                                                                     {
@@ -1441,15 +1444,16 @@ impl Render for LspLogToolbarItemView {
                                                                         );
                                                                     }
                                                                 });
+                                                                }
+                                                            });
+                                                            if option == trace_level {
+                                                                menu.select_last(window, cx);
                                                             }
-                                                        });
-                                                        if option == trace_level {
-                                                            menu.select_last();
                                                         }
-                                                    }
 
-                                                    menu
-                                                })
+                                                        menu
+                                                    },
+                                                )
                                                 .into()
                                             }
                                         }),
@@ -1480,19 +1484,22 @@ impl Render for LspLogToolbarItemView {
                                                         })
                                                     })?;
 
-                                                ContextMenu::build(window, cx, |mut menu, _, _| {
-                                                    let log_view = log_view.clone();
-
-                                                    for (option, label) in [
-                                                        (MessageType::LOG, "Log"),
-                                                        (MessageType::INFO, "Info"),
-                                                        (MessageType::WARNING, "Warning"),
-                                                        (MessageType::ERROR, "Error"),
-                                                    ] {
-                                                        menu = menu.entry(label, None, {
-                                                            let log_view = log_view.clone();
-                                                            move |window, cx| {
-                                                                log_view.update(cx, |this, cx| {
+                                                ContextMenu::build(
+                                                    window,
+                                                    cx,
+                                                    |mut menu, window, cx| {
+                                                        let log_view = log_view.clone();
+
+                                                        for (option, label) in [
+                                                            (MessageType::LOG, "Log"),
+                                                            (MessageType::INFO, "Info"),
+                                                            (MessageType::WARNING, "Warning"),
+                                                            (MessageType::ERROR, "Error"),
+                                                        ] {
+                                                            menu = menu.entry(label, None, {
+                                                                let log_view = log_view.clone();
+                                                                move |window, cx| {
+                                                                    log_view.update(cx, |this, cx| {
                                                                     if let Some(id) =
                                                                         this.current_server_id
                                                                     {
@@ -1501,15 +1508,16 @@ impl Render for LspLogToolbarItemView {
                                                                         );
                                                                     }
                                                                 });
+                                                                }
+                                                            });
+                                                            if option == log_level {
+                                                                menu.select_last(window, cx);
                                                             }
-                                                        });
-                                                        if option == log_level {
-                                                            menu.select_last();
                                                         }
-                                                    }
 
-                                                    menu
-                                                })
+                                                        menu
+                                                    },
+                                                )
                                                 .into()
                                             }
                                         }),

crates/ui/src/components/context_menu.rs 🔗

@@ -135,6 +135,7 @@ pub struct ContextMenu {
     clicked: bool,
     _on_blur_subscription: Subscription,
     keep_open_on_confirm: bool,
+    eager: bool,
     documentation_aside: Option<(usize, Rc<dyn Fn(&mut App) -> AnyElement>)>,
 }
 
@@ -173,6 +174,7 @@ impl ContextMenu {
                     clicked: false,
                     _on_blur_subscription,
                     keep_open_on_confirm: false,
+                    eager: false,
                     documentation_aside: None,
                 },
                 window,
@@ -212,6 +214,40 @@ impl ContextMenu {
                     clicked: false,
                     _on_blur_subscription,
                     keep_open_on_confirm: true,
+                    eager: false,
+                    documentation_aside: None,
+                },
+                window,
+                cx,
+            )
+        })
+    }
+
+    pub fn build_eager(
+        window: &mut Window,
+        cx: &mut App,
+        f: impl FnOnce(Self, &mut Window, &mut Context<Self>) -> Self,
+    ) -> Entity<Self> {
+        cx.new(|cx| {
+            let focus_handle = cx.focus_handle();
+            let _on_blur_subscription = cx.on_blur(
+                &focus_handle,
+                window,
+                |this: &mut ContextMenu, window, cx| this.cancel(&menu::Cancel, window, cx),
+            );
+            window.refresh();
+            f(
+                Self {
+                    builder: None,
+                    items: Default::default(),
+                    focus_handle,
+                    action_context: None,
+                    selected_index: None,
+                    delayed: false,
+                    clicked: false,
+                    _on_blur_subscription,
+                    keep_open_on_confirm: false,
+                    eager: true,
                     documentation_aside: None,
                 },
                 window,
@@ -249,6 +285,7 @@ impl ContextMenu {
                     |this: &mut ContextMenu, window, cx| this.cancel(&menu::Cancel, window, cx),
                 ),
                 keep_open_on_confirm: false,
+                eager: false,
                 documentation_aside: None,
             },
             window,
@@ -435,7 +472,10 @@ impl ContextMenu {
                 ..
             })
             | ContextMenuItem::CustomEntry { handler, .. },
-        ) = self.selected_index.and_then(|ix| self.items.get(ix))
+        ) = self
+            .selected_index
+            .and_then(|ix| self.items.get(ix))
+            .filter(|_| !self.eager)
         {
             (handler)(context, window, cx)
         }
@@ -452,24 +492,24 @@ impl ContextMenu {
         cx.emit(DismissEvent);
     }
 
-    fn select_first(&mut self, _: &SelectFirst, _: &mut Window, cx: &mut Context<Self>) {
+    fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
         if let Some(ix) = self.items.iter().position(|item| item.is_selectable()) {
-            self.select_index(ix);
+            self.select_index(ix, window, cx);
         }
         cx.notify();
     }
 
-    pub fn select_last(&mut self) -> Option<usize> {
+    pub fn select_last(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Option<usize> {
         for (ix, item) in self.items.iter().enumerate().rev() {
             if item.is_selectable() {
-                return self.select_index(ix);
+                return self.select_index(ix, window, cx);
             }
         }
         None
     }
 
-    fn handle_select_last(&mut self, _: &SelectLast, _: &mut Window, cx: &mut Context<Self>) {
-        if self.select_last().is_some() {
+    fn handle_select_last(&mut self, _: &SelectLast, window: &mut Window, cx: &mut Context<Self>) {
+        if self.select_last(window, cx).is_some() {
             cx.notify();
         }
     }
@@ -482,7 +522,7 @@ impl ContextMenu {
             } else {
                 for (ix, item) in self.items.iter().enumerate().skip(next_index) {
                     if item.is_selectable() {
-                        self.select_index(ix);
+                        self.select_index(ix, window, cx);
                         cx.notify();
                         break;
                     }
@@ -505,7 +545,7 @@ impl ContextMenu {
             } else {
                 for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
                     if item.is_selectable() {
-                        self.select_index(ix);
+                        self.select_index(ix, window, cx);
                         cx.notify();
                         break;
                     }
@@ -516,7 +556,13 @@ impl ContextMenu {
         }
     }
 
-    fn select_index(&mut self, ix: usize) -> Option<usize> {
+    fn select_index(
+        &mut self,
+        ix: usize,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Option<usize> {
+        let context = self.action_context.as_ref();
         self.documentation_aside = None;
         let item = self.items.get(ix)?;
         if item.is_selectable() {
@@ -525,6 +571,9 @@ impl ContextMenu {
                 if let Some(callback) = &entry.documentation_aside {
                     self.documentation_aside = Some((ix, callback.clone()));
                 }
+                if self.eager && !entry.disabled {
+                    (entry.handler)(context, window, cx)
+                }
             }
         }
         Some(ix)
@@ -553,7 +602,7 @@ impl ContextMenu {
                 false
             }
         }) {
-            self.select_index(ix);
+            self.select_index(ix, window, cx);
             self.delayed = true;
             cx.notify();
             let action = dispatched.boxed_clone();