agent: Add new panel navigation dropdown (#29539)
- [x] Ensure what appears in the dropdown is really what is accurate - [x] Ensure keyboard navigation works: - [x] Switching tabs with `enter` - [x] Closing items from the menu item - [x] Opening the dropdown - [x] Focus assistant panel on dismiss - [x] Add ability to close items from the dropdown menu - [x] Persistence - [x] Correct behavior when opening a text thread Release Notes: - agent: Added a navigation menu that shows the recently opened threads. The button to see the full history view has been changed inside this menu. --------- Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de> Co-authored-by: Cole Miller <m@cole-miller.net> Co-authored-by: Bennet Bo Fenner <bennet@zed.dev> Co-authored-by: Cole Miller <cole@zed.dev>
This commit is contained in:
parent
1a4d7249f6
commit
b1395c5fdf
17 changed files with 740 additions and 229 deletions
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
Icon, IconName, IconSize, KeyBinding, Label, List, ListItem, ListSeparator, ListSubHeader,
|
||||
h_flex, prelude::*, utils::WithRemSize, v_flex,
|
||||
Icon, IconButtonShape, IconName, IconSize, KeyBinding, Label, List, ListItem, ListSeparator,
|
||||
ListSubHeader, h_flex, prelude::*, utils::WithRemSize, v_flex,
|
||||
};
|
||||
use gpui::{
|
||||
Action, AnyElement, App, AppContext as _, DismissEvent, Entity, EventEmitter, FocusHandle,
|
||||
|
@ -11,6 +11,8 @@ use settings::Settings;
|
|||
use std::{rc::Rc, time::Duration};
|
||||
use theme::ThemeSettings;
|
||||
|
||||
use super::Tooltip;
|
||||
|
||||
pub enum ContextMenuItem {
|
||||
Separator,
|
||||
Header(SharedString),
|
||||
|
@ -47,6 +49,9 @@ pub struct ContextMenuEntry {
|
|||
action: Option<Box<dyn Action>>,
|
||||
disabled: bool,
|
||||
documentation_aside: Option<Rc<dyn Fn(&mut App) -> AnyElement>>,
|
||||
end_slot_icon: Option<IconName>,
|
||||
end_slot_title: Option<SharedString>,
|
||||
end_slot_handler: Option<Rc<dyn Fn(Option<&FocusHandle>, &mut Window, &mut App)>>,
|
||||
}
|
||||
|
||||
impl ContextMenuEntry {
|
||||
|
@ -62,6 +67,9 @@ impl ContextMenuEntry {
|
|||
action: None,
|
||||
disabled: false,
|
||||
documentation_aside: None,
|
||||
end_slot_icon: None,
|
||||
end_slot_title: None,
|
||||
end_slot_handler: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -133,10 +141,13 @@ pub struct ContextMenu {
|
|||
selected_index: Option<usize>,
|
||||
delayed: bool,
|
||||
clicked: bool,
|
||||
end_slot_action: Option<Box<dyn Action>>,
|
||||
key_context: SharedString,
|
||||
_on_blur_subscription: Subscription,
|
||||
keep_open_on_confirm: bool,
|
||||
eager: bool,
|
||||
documentation_aside: Option<(usize, Rc<dyn Fn(&mut App) -> AnyElement>)>,
|
||||
fixed_width: Option<DefiniteLength>,
|
||||
}
|
||||
|
||||
impl Focusable for ContextMenu {
|
||||
|
@ -172,10 +183,13 @@ impl ContextMenu {
|
|||
selected_index: None,
|
||||
delayed: false,
|
||||
clicked: false,
|
||||
key_context: "menu".into(),
|
||||
_on_blur_subscription,
|
||||
keep_open_on_confirm: false,
|
||||
eager: false,
|
||||
documentation_aside: None,
|
||||
fixed_width: None,
|
||||
end_slot_action: None,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
|
@ -212,10 +226,13 @@ impl ContextMenu {
|
|||
selected_index: None,
|
||||
delayed: false,
|
||||
clicked: false,
|
||||
key_context: "menu".into(),
|
||||
_on_blur_subscription,
|
||||
keep_open_on_confirm: true,
|
||||
eager: false,
|
||||
documentation_aside: None,
|
||||
fixed_width: None,
|
||||
end_slot_action: None,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
|
@ -245,10 +262,13 @@ impl ContextMenu {
|
|||
selected_index: None,
|
||||
delayed: false,
|
||||
clicked: false,
|
||||
key_context: "menu".into(),
|
||||
_on_blur_subscription,
|
||||
keep_open_on_confirm: false,
|
||||
eager: true,
|
||||
documentation_aside: None,
|
||||
fixed_width: None,
|
||||
end_slot_action: None,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
|
@ -263,7 +283,7 @@ impl ContextMenu {
|
|||
///
|
||||
/// This only works if the [`ContextMenu`] was constructed using [`ContextMenu::build_persistent`]. Otherwise it is
|
||||
/// a no-op.
|
||||
fn rebuild(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
pub fn rebuild(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(builder) = self.builder.clone() else {
|
||||
return;
|
||||
};
|
||||
|
@ -279,6 +299,7 @@ impl ContextMenu {
|
|||
selected_index: None,
|
||||
delayed: false,
|
||||
clicked: false,
|
||||
key_context: "menu".into(),
|
||||
_on_blur_subscription: cx.on_blur(
|
||||
&focus_handle,
|
||||
window,
|
||||
|
@ -287,6 +308,8 @@ impl ContextMenu {
|
|||
keep_open_on_confirm: false,
|
||||
eager: false,
|
||||
documentation_aside: None,
|
||||
fixed_width: None,
|
||||
end_slot_action: None,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
|
@ -339,6 +362,36 @@ impl ContextMenu {
|
|||
action,
|
||||
disabled: false,
|
||||
documentation_aside: None,
|
||||
end_slot_icon: None,
|
||||
end_slot_title: None,
|
||||
end_slot_handler: None,
|
||||
}));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn entry_with_end_slot(
|
||||
mut self,
|
||||
label: impl Into<SharedString>,
|
||||
action: Option<Box<dyn Action>>,
|
||||
handler: impl Fn(&mut Window, &mut App) + 'static,
|
||||
end_slot_icon: IconName,
|
||||
end_slot_title: SharedString,
|
||||
end_slot_handler: impl Fn(&mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
|
||||
toggle: None,
|
||||
label: label.into(),
|
||||
handler: Rc::new(move |_, window, cx| handler(window, cx)),
|
||||
icon: None,
|
||||
icon_position: IconPosition::End,
|
||||
icon_size: IconSize::Small,
|
||||
icon_color: None,
|
||||
action,
|
||||
disabled: false,
|
||||
documentation_aside: None,
|
||||
end_slot_icon: Some(end_slot_icon),
|
||||
end_slot_title: Some(end_slot_title),
|
||||
end_slot_handler: Some(Rc::new(move |_, window, cx| end_slot_handler(window, cx))),
|
||||
}));
|
||||
self
|
||||
}
|
||||
|
@ -362,6 +415,9 @@ impl ContextMenu {
|
|||
action,
|
||||
disabled: false,
|
||||
documentation_aside: None,
|
||||
end_slot_icon: None,
|
||||
end_slot_title: None,
|
||||
end_slot_handler: None,
|
||||
}));
|
||||
self
|
||||
}
|
||||
|
@ -413,6 +469,9 @@ impl ContextMenu {
|
|||
icon_color: None,
|
||||
disabled: false,
|
||||
documentation_aside: None,
|
||||
end_slot_icon: None,
|
||||
end_slot_title: None,
|
||||
end_slot_handler: None,
|
||||
}));
|
||||
self
|
||||
}
|
||||
|
@ -438,6 +497,9 @@ impl ContextMenu {
|
|||
icon_color: None,
|
||||
disabled: true,
|
||||
documentation_aside: None,
|
||||
end_slot_icon: None,
|
||||
end_slot_title: None,
|
||||
end_slot_handler: None,
|
||||
}));
|
||||
self
|
||||
}
|
||||
|
@ -454,12 +516,43 @@ impl ContextMenu {
|
|||
icon_color: None,
|
||||
disabled: false,
|
||||
documentation_aside: None,
|
||||
end_slot_icon: None,
|
||||
end_slot_title: None,
|
||||
end_slot_handler: None,
|
||||
}));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn keep_open_on_confirm(mut self) -> Self {
|
||||
self.keep_open_on_confirm = true;
|
||||
pub fn keep_open_on_confirm(mut self, keep_open: bool) -> Self {
|
||||
self.keep_open_on_confirm = keep_open;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn trigger_end_slot_handler(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(entry) = self.selected_index.and_then(|ix| self.items.get(ix)) else {
|
||||
return;
|
||||
};
|
||||
let ContextMenuItem::Entry(entry) = entry else {
|
||||
return;
|
||||
};
|
||||
let Some(handler) = entry.end_slot_handler.as_ref() else {
|
||||
return;
|
||||
};
|
||||
handler(None, window, cx);
|
||||
}
|
||||
|
||||
pub fn fixed_width(mut self, width: DefiniteLength) -> Self {
|
||||
self.fixed_width = Some(width);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn end_slot_action(mut self, action: Box<dyn Action>) -> Self {
|
||||
self.end_slot_action = Some(action);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn key_context(mut self, context: impl Into<SharedString>) -> Self {
|
||||
self.key_context = context.into();
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -492,6 +585,25 @@ impl ContextMenu {
|
|||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
pub fn end_slot(&mut self, _: &dyn Action, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(item) = self.selected_index.and_then(|ix| self.items.get(ix)) else {
|
||||
return;
|
||||
};
|
||||
let ContextMenuItem::Entry(entry) = item else {
|
||||
return;
|
||||
};
|
||||
let Some(handler) = entry.end_slot_handler.as_ref() else {
|
||||
return;
|
||||
};
|
||||
handler(None, window, cx);
|
||||
self.rebuild(window, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn clear_selected(&mut self) {
|
||||
self.selected_index = None;
|
||||
}
|
||||
|
||||
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, window, cx);
|
||||
|
@ -707,7 +819,11 @@ impl ContextMenu {
|
|||
action,
|
||||
disabled,
|
||||
documentation_aside,
|
||||
end_slot_icon,
|
||||
end_slot_title,
|
||||
end_slot_handler,
|
||||
} = entry;
|
||||
let this = cx.weak_entity();
|
||||
|
||||
let handler = handler.clone();
|
||||
let menu = cx.entity().downgrade();
|
||||
|
@ -733,7 +849,7 @@ impl ContextMenu {
|
|||
*icon_position == IconPosition::Start && toggle.is_none(),
|
||||
|flex| flex.child(Icon::new(*icon_name).size(*icon_size).color(icon_color)),
|
||||
)
|
||||
.child(Label::new(label.clone()).color(label_color))
|
||||
.child(Label::new(label.clone()).color(label_color).truncate())
|
||||
.when(*icon_position == IconPosition::End, |flex| {
|
||||
flex.child(Icon::new(*icon_name).size(*icon_size).color(icon_color))
|
||||
})
|
||||
|
@ -741,6 +857,7 @@ impl ContextMenu {
|
|||
} else {
|
||||
Label::new(label.clone())
|
||||
.color(label_color)
|
||||
.truncate()
|
||||
.into_any_element()
|
||||
};
|
||||
|
||||
|
@ -818,6 +935,56 @@ impl ContextMenu {
|
|||
},
|
||||
),
|
||||
)
|
||||
.when_some(
|
||||
end_slot_icon
|
||||
.as_ref()
|
||||
.zip(self.end_slot_action.as_ref())
|
||||
.zip(end_slot_title.as_ref())
|
||||
.zip(end_slot_handler.as_ref()),
|
||||
|el, (((icon, action), title), handler)| {
|
||||
el.end_slot(
|
||||
IconButton::new("end-slot-icon", *icon)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip({
|
||||
let action_context = self.action_context.clone();
|
||||
let title = title.clone();
|
||||
let action = action.boxed_clone();
|
||||
move |window, cx| {
|
||||
action_context
|
||||
.as_ref()
|
||||
.map(|focus| {
|
||||
Tooltip::for_action_in(
|
||||
title.clone(),
|
||||
&*action,
|
||||
focus,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
Tooltip::for_action(
|
||||
title.clone(),
|
||||
&*action,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
.on_click({
|
||||
let handler = handler.clone();
|
||||
move |_, window, cx| {
|
||||
handler(None, window, cx);
|
||||
this.update(cx, |this, cx| {
|
||||
this.rebuild(window, cx);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}),
|
||||
)
|
||||
},
|
||||
)
|
||||
.on_click({
|
||||
let context = self.action_context.clone();
|
||||
let keep_open_on_confirm = self.keep_open_on_confirm;
|
||||
|
@ -888,21 +1055,28 @@ impl Render for ContextMenu {
|
|||
.child(
|
||||
v_flex()
|
||||
.id("context-menu")
|
||||
.min_w(px(200.))
|
||||
.max_h(vh(0.75, window))
|
||||
.flex_1()
|
||||
.when_some(self.fixed_width, |this, width| {
|
||||
this.w(width).overflow_x_hidden()
|
||||
})
|
||||
.when(self.fixed_width.is_none(), |this| {
|
||||
this.min_w(px(200.)).flex_1()
|
||||
})
|
||||
.overflow_y_scroll()
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.on_mouse_down_out(cx.listener(|this, _, window, cx| {
|
||||
this.cancel(&menu::Cancel, window, cx)
|
||||
}))
|
||||
.key_context("menu")
|
||||
.key_context(self.key_context.as_ref())
|
||||
.on_action(cx.listener(ContextMenu::select_first))
|
||||
.on_action(cx.listener(ContextMenu::handle_select_last))
|
||||
.on_action(cx.listener(ContextMenu::select_next))
|
||||
.on_action(cx.listener(ContextMenu::select_previous))
|
||||
.on_action(cx.listener(ContextMenu::confirm))
|
||||
.on_action(cx.listener(ContextMenu::cancel))
|
||||
.when_some(self.end_slot_action.as_ref(), |el, action| {
|
||||
el.on_boxed_action(&**action, cx.listener(ContextMenu::end_slot))
|
||||
})
|
||||
.when(!self.delayed, |mut el| {
|
||||
for item in self.items.iter() {
|
||||
if let ContextMenuItem::Entry(ContextMenuEntry {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue