Compare commits

...

2 Commits

Author SHA1 Message Date
Nate Butler
3772a467e0 Remove logs 2024-10-07 15:14:54 -04:00
Nate Butler
1be27e7270 wip first approach at advanced entries 2024-10-07 15:14:34 -04:00
2 changed files with 383 additions and 129 deletions

View File

@@ -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())
})

View File

@@ -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()
}
})
},
)),
),
),
)
}