Progress on ContextMenu

This commit is contained in:
Conrad Irwin 2023-11-16 16:59:27 -07:00
parent 9456f716c2
commit 074a221e0f
5 changed files with 247 additions and 158 deletions

View file

@ -1,5 +1,13 @@
use crate::{prelude::*, ListItemVariant};
use std::cell::RefCell;
use std::rc::Rc;
use crate::{h_stack, prelude::*, ListItemVariant};
use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHeader};
use gpui::{
overlay, px, Action, AnyElement, Bounds, DispatchPhase, Div, EventEmitter, FocusHandle,
Focusable, FocusableView, LayoutId, MouseButton, MouseDownEvent, Overlay, Render, View,
};
use smallvec::SmallVec;
pub enum ContextMenuItem {
Header(SharedString),
@ -19,12 +27,12 @@ impl Clone for ContextMenuItem {
}
}
impl ContextMenuItem {
fn to_list_item<V: 'static>(self) -> ListItem {
fn to_list_item(self) -> ListItem {
match self {
ContextMenuItem::Header(label) => ListSubHeader::new(label).into(),
ContextMenuItem::Entry(label, action) => ListEntry::new(label)
.variant(ListItemVariant::Inset)
.on_click(action)
.action(action)
.into(),
ContextMenuItem::Separator => ListSeparator::new().into(),
}
@ -43,40 +51,196 @@ impl ContextMenuItem {
}
}
#[derive(Component, Clone)]
pub struct ContextMenu {
items: Vec<ContextMenuItem>,
items: Vec<ListItem>,
focus_handle: FocusHandle,
}
pub enum MenuEvent {
Dismissed,
}
impl EventEmitter<MenuEvent> for ContextMenu {}
impl FocusableView for ContextMenu {
fn focus_handle(&self, cx: &gpui::AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
impl ContextMenu {
pub fn new(items: impl IntoIterator<Item = ContextMenuItem>) -> Self {
pub fn new(cx: &mut WindowContext) -> Self {
Self {
items: items.into_iter().collect(),
items: Default::default(),
focus_handle: cx.focus_handle(),
}
}
// todo!()
// cx.add_action(ContextMenu::select_first);
// cx.add_action(ContextMenu::select_last);
// cx.add_action(ContextMenu::select_next);
// cx.add_action(ContextMenu::select_prev);
// cx.add_action(ContextMenu::confirm);
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
v_stack()
.flex()
.bg(cx.theme().colors().elevated_surface_background)
.border()
.border_color(cx.theme().colors().border)
.child(List::new(
self.items
.into_iter()
.map(ContextMenuItem::to_list_item::<V>)
.collect(),
))
.on_mouse_down_out(|_, _, cx| cx.dispatch_action(Box::new(menu::Cancel)))
pub fn header(mut self, title: impl Into<SharedString>) -> Self {
self.items.push(ListItem::Header(ListSubHeader::new(title)));
self
}
pub fn separator(mut self) -> Self {
self.items.push(ListItem::Separator(ListSeparator));
self
}
pub fn entry(mut self, label: Label, action: Box<dyn Action>) -> Self {
self.items.push(ListEntry::new(label).action(action).into());
self
}
pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
// todo!()
cx.emit(MenuEvent::Dismissed);
}
pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
cx.emit(MenuEvent::Dismissed);
}
}
impl Render for ContextMenu {
type Element = Overlay<Self>;
// todo!()
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
overlay().child(
div().elevation_2(cx).flex().flex_row().child(
v_stack()
.min_w(px(200.))
.track_focus(&self.focus_handle)
.on_mouse_down_out(|this: &mut Self, _, cx| {
this.cancel(&Default::default(), cx)
})
// .on_action(ContextMenu::select_first)
// .on_action(ContextMenu::select_last)
// .on_action(ContextMenu::select_next)
// .on_action(ContextMenu::select_prev)
.on_action(ContextMenu::confirm)
.on_action(ContextMenu::cancel)
.flex_none()
// .bg(cx.theme().colors().elevated_surface_background)
// .border()
// .border_color(cx.theme().colors().border)
.child(List::new(self.items.clone())),
),
)
}
}
pub struct MenuHandle<V: 'static> {
id: ElementId,
children: SmallVec<[AnyElement<V>; 2]>,
builder: Rc<dyn Fn(&mut V, &mut ViewContext<V>) -> View<ContextMenu> + 'static>,
}
impl<V: 'static> ParentComponent<V> for MenuHandle<V> {
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
&mut self.children
}
}
impl<V: 'static> MenuHandle<V> {
pub fn new(
id: impl Into<ElementId>,
builder: impl Fn(&mut V, &mut ViewContext<V>) -> View<ContextMenu> + 'static,
) -> Self {
Self {
id: id.into(),
children: SmallVec::new(),
builder: Rc::new(builder),
}
}
}
pub struct MenuHandleState<V> {
menu: Rc<RefCell<Option<View<ContextMenu>>>>,
menu_element: Option<AnyElement<V>>,
}
impl<V: 'static> Element<V> for MenuHandle<V> {
type ElementState = MenuHandleState<V>;
fn element_id(&self) -> Option<gpui::ElementId> {
Some(self.id.clone())
}
fn layout(
&mut self,
view_state: &mut V,
element_state: Option<Self::ElementState>,
cx: &mut crate::ViewContext<V>,
) -> (gpui::LayoutId, Self::ElementState) {
let mut child_layout_ids = self
.children
.iter_mut()
.map(|child| child.layout(view_state, cx))
.collect::<SmallVec<[LayoutId; 2]>>();
let menu = if let Some(element_state) = element_state {
element_state.menu
} else {
Rc::new(RefCell::new(None))
};
let menu_element = menu.borrow_mut().as_mut().map(|menu| {
let mut view = menu.clone().render();
child_layout_ids.push(view.layout(view_state, cx));
view
});
let layout_id = cx.request_layout(&gpui::Style::default(), child_layout_ids.into_iter());
(layout_id, MenuHandleState { menu, menu_element })
}
fn paint(
&mut self,
bounds: Bounds<gpui::Pixels>,
view_state: &mut V,
element_state: &mut Self::ElementState,
cx: &mut crate::ViewContext<V>,
) {
for child in &mut self.children {
child.paint(view_state, cx);
}
if let Some(menu) = element_state.menu_element.as_mut() {
menu.paint(view_state, cx);
return;
}
let menu = element_state.menu.clone();
let builder = self.builder.clone();
cx.on_mouse_event(move |view_state, event: &MouseDownEvent, phase, cx| {
if phase == DispatchPhase::Bubble
&& event.button == MouseButton::Right
&& bounds.contains_point(&event.position)
{
cx.stop_propagation();
cx.prevent_default();
let new_menu = (builder)(view_state, cx);
let menu2 = menu.clone();
cx.subscribe(&new_menu, move |this, modal, e, cx| match e {
MenuEvent::Dismissed => {
*menu2.borrow_mut() = None;
cx.notify();
}
})
.detach();
*menu.borrow_mut() = Some(new_menu);
cx.notify();
}
});
}
}
impl<V: 'static> Component<V> for MenuHandle<V> {
fn render(self) -> AnyElement<V> {
AnyElement::new(self)
}
}
use gpui::Action;
#[cfg(feature = "stories")]
pub use stories::*;
@ -84,7 +248,7 @@ pub use stories::*;
mod stories {
use super::*;
use crate::story::Story;
use gpui::{action, Div, Render};
use gpui::{action, Div, Render, VisualContext};
pub struct ContextMenuStory;
@ -97,17 +261,25 @@ mod stories {
Story::container(cx)
.child(Story::title_for::<_, ContextMenu>(cx))
.child(Story::label(cx, "Default"))
.child(ContextMenu::new([
ContextMenuItem::header("Section header"),
ContextMenuItem::Separator,
ContextMenuItem::entry(Label::new("Print current time"), PrintCurrentDate {}),
]))
.on_action(|_, _: &PrintCurrentDate, _| {
if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() {
println!("Current Unix time is {:?}", unix_time.as_secs());
}
})
.child(
MenuHandle::new("test", move |_, cx| {
cx.build_view(|cx| {
ContextMenu::new(cx)
.header("Section header")
.separator()
.entry(
Label::new("Print current time"),
PrintCurrentDate {}.boxed_clone(),
)
})
})
.child(Label::new("RIGHT CLICK ME")),
)
}
}
}