Compare commits
2 Commits
fix-git-ht
...
context-me
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3772a467e0 | ||
|
|
1be27e7270 |
@@ -584,7 +584,17 @@ impl TitleBar {
|
||||
.separator()
|
||||
})
|
||||
.action("Settings", zed_actions::OpenSettings.boxed_clone())
|
||||
.advanced_action(
|
||||
"Default Settings",
|
||||
zed_actions::OpenSettings.boxed_clone(),
|
||||
Some("Settings".into()),
|
||||
)
|
||||
.action("Key Bindings", Box::new(zed_actions::OpenKeymap))
|
||||
.advanced_action(
|
||||
"Some Advanced",
|
||||
Box::new(zed_actions::DecreaseUiFontSize),
|
||||
None,
|
||||
)
|
||||
.action("Themes…", theme_selector::Toggle::default().boxed_clone())
|
||||
.action("Extensions", extensions_ui::Extensions.boxed_clone())
|
||||
.separator()
|
||||
@@ -613,7 +623,17 @@ impl TitleBar {
|
||||
.menu(|cx| {
|
||||
ContextMenu::build(cx, |menu, _| {
|
||||
menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
|
||||
.advanced_action(
|
||||
"Default Settings",
|
||||
zed_actions::OpenSettings.boxed_clone(),
|
||||
Some("Settings".into()),
|
||||
)
|
||||
.action("Key Bindings", Box::new(zed_actions::OpenKeymap))
|
||||
.advanced_action(
|
||||
"Some Advanced",
|
||||
Box::new(zed_actions::DecreaseUiFontSize),
|
||||
None,
|
||||
)
|
||||
.action("Themes…", theme_selector::Toggle::default().boxed_clone())
|
||||
.action("Extensions", extensions_ui::Extensions.boxed_clone())
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::{
|
||||
};
|
||||
use gpui::{
|
||||
px, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
|
||||
IntoElement, Render, Subscription, View, VisualContext,
|
||||
IntoElement, Modifiers, ModifiersChangedEvent, Render, Subscription, View, VisualContext,
|
||||
};
|
||||
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
|
||||
use settings::Settings;
|
||||
@@ -24,6 +24,16 @@ enum ContextMenuItem {
|
||||
action: Option<Box<dyn Action>>,
|
||||
disabled: bool,
|
||||
},
|
||||
AdvancedEntry {
|
||||
toggle: Option<(IconPosition, bool)>,
|
||||
label: SharedString,
|
||||
icon: Option<IconName>,
|
||||
handler: Rc<dyn Fn(Option<&FocusHandle>, &mut WindowContext)>,
|
||||
action: Option<Box<dyn Action>>,
|
||||
disabled: bool,
|
||||
/// Allows the advanced entry to specify a entry to hide when it is visible, enabling a "swapping" effect between the two.
|
||||
hide_other_entry_when_visible: Option<SharedString>,
|
||||
},
|
||||
CustomEntry {
|
||||
entry_render: Box<dyn Fn(&mut WindowContext) -> AnyElement>,
|
||||
handler: Rc<dyn Fn(Option<&FocusHandle>, &mut WindowContext)>,
|
||||
@@ -33,11 +43,14 @@ enum ContextMenuItem {
|
||||
|
||||
pub struct ContextMenu {
|
||||
items: Vec<ContextMenuItem>,
|
||||
item_visibility: Vec<bool>,
|
||||
focus_handle: FocusHandle,
|
||||
action_context: Option<FocusHandle>,
|
||||
selected_index: Option<usize>,
|
||||
show_advanced: bool,
|
||||
delayed: bool,
|
||||
clicked: bool,
|
||||
init_modifiers: Option<Modifiers>,
|
||||
_on_blur_subscription: Subscription,
|
||||
}
|
||||
|
||||
@@ -62,18 +75,22 @@ impl ContextMenu {
|
||||
this.cancel(&menu::Cancel, cx)
|
||||
});
|
||||
cx.refresh();
|
||||
f(
|
||||
Self {
|
||||
items: Default::default(),
|
||||
focus_handle,
|
||||
action_context: None,
|
||||
selected_index: None,
|
||||
delayed: false,
|
||||
clicked: false,
|
||||
_on_blur_subscription,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
let mut menu = Self {
|
||||
items: Default::default(),
|
||||
item_visibility: Default::default(),
|
||||
focus_handle,
|
||||
action_context: None,
|
||||
selected_index: None,
|
||||
delayed: false,
|
||||
clicked: false,
|
||||
show_advanced: false,
|
||||
init_modifiers: None,
|
||||
_on_blur_subscription,
|
||||
};
|
||||
menu = f(menu, cx);
|
||||
menu.item_visibility = vec![true; menu.items.len()];
|
||||
menu.handle_update_items(cx);
|
||||
menu
|
||||
})
|
||||
}
|
||||
|
||||
@@ -109,6 +126,26 @@ impl ContextMenu {
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds an entry that is only visible when `show_advanced` is enabled.
|
||||
pub fn advanced_entry(
|
||||
mut self,
|
||||
label: impl Into<SharedString>,
|
||||
action: Option<Box<dyn Action>>,
|
||||
handler: impl Fn(&mut WindowContext) + 'static,
|
||||
hidden_entry_when_visible: impl Into<Option<SharedString>>,
|
||||
) -> Self {
|
||||
self.items.push(ContextMenuItem::AdvancedEntry {
|
||||
toggle: None,
|
||||
label: label.into(),
|
||||
handler: Rc::new(move |_, cx| handler(cx)),
|
||||
icon: None,
|
||||
action,
|
||||
disabled: false,
|
||||
hide_other_entry_when_visible: hidden_entry_when_visible.into(),
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
pub fn toggleable_entry(
|
||||
mut self,
|
||||
label: impl Into<SharedString>,
|
||||
@@ -176,6 +213,30 @@ impl ContextMenu {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn advanced_action(
|
||||
mut self,
|
||||
label: impl Into<SharedString>,
|
||||
action: Box<dyn Action>,
|
||||
hidden_entry_when_visible: impl Into<Option<SharedString>>,
|
||||
) -> Self {
|
||||
self.items.push(ContextMenuItem::AdvancedEntry {
|
||||
toggle: None,
|
||||
label: label.into(),
|
||||
action: Some(action.boxed_clone()),
|
||||
|
||||
handler: Rc::new(move |context, cx| {
|
||||
if let Some(context) = &context {
|
||||
cx.focus(context);
|
||||
}
|
||||
cx.dispatch_action(action.boxed_clone());
|
||||
}),
|
||||
icon: None,
|
||||
disabled: false,
|
||||
hide_other_entry_when_visible: hidden_entry_when_visible.into(),
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
pub fn disabled_action(
|
||||
mut self,
|
||||
label: impl Into<SharedString>,
|
||||
@@ -323,6 +384,64 @@ impl ContextMenu {
|
||||
self._on_blur_subscription = new_subscription;
|
||||
self
|
||||
}
|
||||
|
||||
fn handle_modifiers_changed(
|
||||
&mut self,
|
||||
event: &ModifiersChangedEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let Some(init_modifiers) = self.init_modifiers else {
|
||||
self.init_modifiers = Some(event.modifiers);
|
||||
return;
|
||||
};
|
||||
|
||||
if event.modifiers.alt != init_modifiers.alt {
|
||||
self.show_advanced(event.modifiers.alt, cx);
|
||||
}
|
||||
|
||||
self.init_modifiers = Some(event.modifiers);
|
||||
}
|
||||
|
||||
fn show_advanced(&mut self, show_advanced: bool, cx: &mut ViewContext<Self>) {
|
||||
self.show_advanced = show_advanced;
|
||||
self.handle_update_items(cx);
|
||||
}
|
||||
|
||||
fn handle_update_items(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.item_visibility.clear();
|
||||
self.item_visibility.reserve(self.items.len());
|
||||
|
||||
for item in &self.items {
|
||||
match item {
|
||||
ContextMenuItem::AdvancedEntry {
|
||||
hide_other_entry_when_visible,
|
||||
..
|
||||
} => {
|
||||
let visible = self.show_advanced;
|
||||
self.item_visibility.push(visible);
|
||||
if visible {
|
||||
if let Some(hidden_label) = hide_other_entry_when_visible {
|
||||
// Hide the corresponding entry
|
||||
if let Some(pos) = self.items.iter().position(|i| {
|
||||
matches!(i, ContextMenuItem::Entry { label, .. } if label == hidden_label)
|
||||
}) {
|
||||
self.item_visibility[pos] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ContextMenuItem::Entry { label, .. } => {
|
||||
let visible = !self.show_advanced || !self.items.iter().any(|i| {
|
||||
matches!(i, ContextMenuItem::AdvancedEntry { hide_other_entry_when_visible: Some(hidden), .. } if hidden == label)
|
||||
});
|
||||
self.item_visibility.push(visible);
|
||||
}
|
||||
_ => self.item_visibility.push(true),
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextMenuItem {
|
||||
@@ -332,6 +451,7 @@ impl ContextMenuItem {
|
||||
| ContextMenuItem::Separator
|
||||
| ContextMenuItem::Label { .. } => false,
|
||||
ContextMenuItem::Entry { disabled, .. } => !disabled,
|
||||
ContextMenuItem::AdvancedEntry { disabled, .. } => !disabled,
|
||||
ContextMenuItem::CustomEntry { selectable, .. } => *selectable,
|
||||
}
|
||||
}
|
||||
@@ -357,6 +477,7 @@ impl Render for ContextMenu {
|
||||
.on_action(cx.listener(ContextMenu::select_prev))
|
||||
.on_action(cx.listener(ContextMenu::confirm))
|
||||
.on_action(cx.listener(ContextMenu::cancel))
|
||||
.on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
|
||||
.when(!self.delayed, |mut el| {
|
||||
for item in self.items.iter() {
|
||||
if let ContextMenuItem::Entry {
|
||||
@@ -374,119 +495,106 @@ impl Render for ContextMenu {
|
||||
el
|
||||
})
|
||||
.flex_none()
|
||||
.child(List::new().children(self.items.iter_mut().enumerate().map(
|
||||
|(ix, item)| {
|
||||
match item {
|
||||
ContextMenuItem::Separator => ListSeparator.into_any_element(),
|
||||
ContextMenuItem::Header(header) => {
|
||||
ListSubHeader::new(header.clone())
|
||||
.inset(true)
|
||||
.into_any_element()
|
||||
.child(
|
||||
List::new().children(self.items.iter_mut().enumerate().filter_map(
|
||||
|(ix, item)| {
|
||||
if !self.item_visibility[ix] {
|
||||
return None;
|
||||
}
|
||||
ContextMenuItem::Label(label) => ListItem::new(ix)
|
||||
.inset(true)
|
||||
.disabled(true)
|
||||
.child(Label::new(label.clone()))
|
||||
.into_any_element(),
|
||||
ContextMenuItem::Entry {
|
||||
toggle,
|
||||
label,
|
||||
handler,
|
||||
icon,
|
||||
action,
|
||||
disabled,
|
||||
} => {
|
||||
let handler = handler.clone();
|
||||
let menu = cx.view().downgrade();
|
||||
let color = if *disabled {
|
||||
Color::Muted
|
||||
} else {
|
||||
Color::Default
|
||||
};
|
||||
let label_element = if let Some(icon) = icon {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Label::new(label.clone()).color(color))
|
||||
.child(
|
||||
Icon::new(*icon).size(IconSize::Small).color(color),
|
||||
)
|
||||
Some(match item {
|
||||
ContextMenuItem::Separator => ListSeparator.into_any_element(),
|
||||
ContextMenuItem::Header(header) => {
|
||||
ListSubHeader::new(header.clone())
|
||||
.inset(true)
|
||||
.into_any_element()
|
||||
} else {
|
||||
Label::new(label.clone()).color(color).into_any_element()
|
||||
};
|
||||
|
||||
ListItem::new(ix)
|
||||
}
|
||||
ContextMenuItem::Label(label) => ListItem::new(ix)
|
||||
.inset(true)
|
||||
.disabled(*disabled)
|
||||
.selected(Some(ix) == self.selected_index)
|
||||
.when_some(*toggle, |list_item, (position, toggled)| {
|
||||
let contents = if toggled {
|
||||
v_flex().flex_none().child(
|
||||
Icon::new(IconName::Check).color(Color::Accent),
|
||||
)
|
||||
} else {
|
||||
v_flex()
|
||||
.flex_none()
|
||||
.size(IconSize::default().rems())
|
||||
};
|
||||
match position {
|
||||
IconPosition::Start => {
|
||||
list_item.start_slot(contents)
|
||||
}
|
||||
IconPosition::End => list_item.end_slot(contents),
|
||||
}
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(label_element)
|
||||
.debug_selector(|| format!("MENU_ITEM-{}", label))
|
||||
.children(action.as_ref().and_then(|action| {
|
||||
self.action_context
|
||||
.as_ref()
|
||||
.map(|focus| {
|
||||
KeyBinding::for_action_in(
|
||||
&**action, focus, cx,
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
KeyBinding::for_action(&**action, cx)
|
||||
})
|
||||
.map(|binding| div().ml_4().child(binding))
|
||||
})),
|
||||
)
|
||||
.on_click({
|
||||
let context = self.action_context.clone();
|
||||
move |_, cx| {
|
||||
handler(context.as_ref(), cx);
|
||||
menu.update(cx, |menu, cx| {
|
||||
menu.clicked = true;
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
ContextMenuItem::CustomEntry {
|
||||
entry_render,
|
||||
handler,
|
||||
selectable,
|
||||
} => {
|
||||
let handler = handler.clone();
|
||||
let menu = cx.view().downgrade();
|
||||
let selectable = *selectable;
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.selected(if selectable {
|
||||
Some(ix) == self.selected_index
|
||||
.disabled(true)
|
||||
.child(Label::new(label.clone()))
|
||||
.into_any_element(),
|
||||
ContextMenuItem::Entry {
|
||||
toggle,
|
||||
label,
|
||||
handler,
|
||||
icon,
|
||||
action,
|
||||
disabled,
|
||||
} => {
|
||||
let handler = handler.clone();
|
||||
let menu = cx.view().downgrade();
|
||||
let color = if *disabled {
|
||||
Color::Muted
|
||||
} else {
|
||||
false
|
||||
})
|
||||
.selectable(selectable)
|
||||
.when(selectable, |item| {
|
||||
item.on_click({
|
||||
Color::Default
|
||||
};
|
||||
let label_element = if let Some(icon) = icon {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Label::new(label.clone()).color(color))
|
||||
.child(
|
||||
Icon::new(*icon)
|
||||
.size(IconSize::Small)
|
||||
.color(color),
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
Label::new(label.clone())
|
||||
.color(color)
|
||||
.into_any_element()
|
||||
};
|
||||
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.disabled(*disabled)
|
||||
.selected(Some(ix) == self.selected_index)
|
||||
.when_some(*toggle, |list_item, (position, toggled)| {
|
||||
let contents = if toggled {
|
||||
v_flex().flex_none().child(
|
||||
Icon::new(IconName::Check)
|
||||
.color(Color::Accent),
|
||||
)
|
||||
} else {
|
||||
v_flex()
|
||||
.flex_none()
|
||||
.size(IconSize::default().rems())
|
||||
};
|
||||
match position {
|
||||
IconPosition::Start => {
|
||||
list_item.start_slot(contents)
|
||||
}
|
||||
IconPosition::End => {
|
||||
list_item.end_slot(contents)
|
||||
}
|
||||
}
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(label_element)
|
||||
.debug_selector(|| {
|
||||
format!("MENU_ITEM-{}", label)
|
||||
})
|
||||
.children(action.as_ref().and_then(|action| {
|
||||
self.action_context
|
||||
.as_ref()
|
||||
.map(|focus| {
|
||||
KeyBinding::for_action_in(
|
||||
&**action, focus, cx,
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
KeyBinding::for_action(
|
||||
&**action, cx,
|
||||
)
|
||||
})
|
||||
.map(|binding| {
|
||||
div().ml_4().child(binding)
|
||||
})
|
||||
})),
|
||||
)
|
||||
.on_click({
|
||||
let context = self.action_context.clone();
|
||||
move |_, cx| {
|
||||
handler(context.as_ref(), cx);
|
||||
@@ -497,13 +605,139 @@ impl Render for ContextMenu {
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
})
|
||||
.child(entry_render(cx))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
},
|
||||
))),
|
||||
.into_any_element()
|
||||
}
|
||||
ContextMenuItem::AdvancedEntry {
|
||||
toggle,
|
||||
label,
|
||||
handler,
|
||||
icon,
|
||||
action,
|
||||
disabled,
|
||||
..
|
||||
} => {
|
||||
let handler = handler.clone();
|
||||
let menu = cx.view().downgrade();
|
||||
let color = if *disabled {
|
||||
Color::Muted
|
||||
} else {
|
||||
Color::Default
|
||||
};
|
||||
let label_element = if let Some(icon) = icon {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Label::new(label.clone()).color(color))
|
||||
.child(
|
||||
Icon::new(*icon)
|
||||
.size(IconSize::Small)
|
||||
.color(color),
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
Label::new(label.clone())
|
||||
.color(color)
|
||||
.into_any_element()
|
||||
};
|
||||
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.disabled(*disabled)
|
||||
.selected(Some(ix) == self.selected_index)
|
||||
.when_some(*toggle, |list_item, (position, toggled)| {
|
||||
let contents = if toggled {
|
||||
v_flex().flex_none().child(
|
||||
Icon::new(IconName::Check)
|
||||
.color(Color::Accent),
|
||||
)
|
||||
} else {
|
||||
v_flex()
|
||||
.flex_none()
|
||||
.size(IconSize::default().rems())
|
||||
};
|
||||
match position {
|
||||
IconPosition::Start => {
|
||||
list_item.start_slot(contents)
|
||||
}
|
||||
IconPosition::End => {
|
||||
list_item.end_slot(contents)
|
||||
}
|
||||
}
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(label_element)
|
||||
.debug_selector(|| {
|
||||
format!("MENU_ITEM-{}", label)
|
||||
})
|
||||
.children(action.as_ref().and_then(|action| {
|
||||
self.action_context
|
||||
.as_ref()
|
||||
.map(|focus| {
|
||||
KeyBinding::for_action_in(
|
||||
&**action, focus, cx,
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
KeyBinding::for_action(
|
||||
&**action, cx,
|
||||
)
|
||||
})
|
||||
.map(|binding| {
|
||||
div().ml_4().child(binding)
|
||||
})
|
||||
})),
|
||||
)
|
||||
.on_click({
|
||||
let context = self.action_context.clone();
|
||||
move |_, cx| {
|
||||
handler(context.as_ref(), cx);
|
||||
menu.update(cx, |menu, cx| {
|
||||
menu.clicked = true;
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
ContextMenuItem::CustomEntry {
|
||||
entry_render,
|
||||
handler,
|
||||
selectable,
|
||||
} => {
|
||||
let handler = handler.clone();
|
||||
let menu = cx.view().downgrade();
|
||||
let selectable = *selectable;
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.selected(if selectable {
|
||||
Some(ix) == self.selected_index
|
||||
} else {
|
||||
false
|
||||
})
|
||||
.selectable(selectable)
|
||||
.when(selectable, |item| {
|
||||
item.on_click({
|
||||
let context = self.action_context.clone();
|
||||
move |_, cx| {
|
||||
handler(context.as_ref(), cx);
|
||||
menu.update(cx, |menu, cx| {
|
||||
menu.clicked = true;
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
})
|
||||
.child(entry_render(cx))
|
||||
.into_any_element()
|
||||
}
|
||||
})
|
||||
},
|
||||
)),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user