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,8 +1,9 @@
use smallvec::SmallVec;
use taffy::style::Position;
use crate::{
point, AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, ParentComponent, Pixels,
Point, Size, Style,
point, px, AbsoluteLength, AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId,
ParentComponent, Pixels, Point, Size, Style,
};
pub struct OverlayState {
@ -72,8 +73,9 @@ impl<V: 'static> Element<V> for Overlay<V> {
.iter_mut()
.map(|child| child.layout(view_state, cx))
.collect::<SmallVec<_>>();
let mut overlay_style = Style::default();
overlay_style.position = crate::Position::Absolute;
overlay_style.position = Position::Absolute;
let layout_id = cx.request_layout(&overlay_style, child_layout_ids.iter().copied());
@ -106,6 +108,7 @@ impl<V: 'static> Element<V> for Overlay<V> {
origin: Point::zero(),
size: cx.viewport_size(),
};
dbg!(bounds, desired, limits);
match self.fit_mode {
OverlayFitMode::SnapToWindow => {

View File

@ -87,7 +87,7 @@ pub struct TerminalView {
has_new_content: bool,
//Currently using iTerm bell, show bell emoji in tab until input is received
has_bell: bool,
context_menu: Option<ContextMenu>,
context_menu: Option<View<ContextMenu>>,
blink_state: bool,
blinking_on: bool,
blinking_paused: bool,
@ -302,10 +302,14 @@ impl TerminalView {
position: gpui::Point<Pixels>,
cx: &mut ViewContext<Self>,
) {
self.context_menu = Some(ContextMenu::new(vec![
ContextMenuItem::entry(Label::new("Clear"), Clear),
ContextMenuItem::entry(Label::new("Close"), CloseActiveItem { save_intent: None }),
]));
self.context_menu = Some(cx.build_view(|cx| {
ContextMenu::new(cx)
.entry(Label::new("Clear"), Box::new(Clear))
.entry(
Label::new("Close"),
Box::new(CloseActiveItem { save_intent: None }),
)
}));
dbg!(&position);
// todo!()
// self.context_menu

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")),
)
}
}
}

View File

@ -117,7 +117,7 @@ impl ListHeader {
}
}
#[derive(Component)]
#[derive(Component, Clone)]
pub struct ListSubHeader {
label: SharedString,
left_icon: Option<Icon>,
@ -172,7 +172,7 @@ pub enum ListEntrySize {
Medium,
}
#[derive(Component)]
#[derive(Component, Clone)]
pub enum ListItem {
Entry(ListEntry),
Separator(ListSeparator),
@ -234,6 +234,24 @@ pub struct ListEntry {
on_click: Option<Box<dyn Action>>,
}
impl Clone for ListEntry {
fn clone(&self) -> Self {
Self {
disabled: self.disabled,
// TODO: Reintroduce this
// disclosure_control_style: DisclosureControlVisibility,
indent_level: self.indent_level,
label: self.label.clone(),
left_slot: self.left_slot.clone(),
overflow: self.overflow,
size: self.size,
toggle: self.toggle,
variant: self.variant,
on_click: self.on_click.as_ref().map(|opt| opt.boxed_clone()),
}
}
}
impl ListEntry {
pub fn new(label: Label) -> Self {
Self {
@ -249,7 +267,7 @@ impl ListEntry {
}
}
pub fn on_click(mut self, action: impl Into<Box<dyn Action>>) -> Self {
pub fn action(mut self, action: impl Into<Box<dyn Action>>) -> Self {
self.on_click = Some(action.into());
self
}

View File

@ -10,7 +10,10 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use std::{cell::RefCell, rc::Rc, sync::Arc};
use ui::{h_stack, IconButton, InteractionState, Label, Tooltip};
use ui::{
h_stack, ContextMenu, ContextMenuItem, IconButton, InteractionState, Label, MenuEvent,
MenuHandle, Tooltip,
};
pub enum PanelEvent {
ChangePosition,
@ -659,117 +662,6 @@ impl PanelButtons {
// }
// }
pub struct MenuHandle<V: 'static> {
id: ElementId,
children: SmallVec<[AnyElement<V>; 2]>,
builder: Rc<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyView + '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> {
fn new(
id: impl Into<ElementId>,
builder: impl Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static,
) -> Self {
Self {
id: id.into(),
children: SmallVec::new(),
builder: Rc::new(builder),
}
}
}
pub struct MenuState<V> {
open: Rc<RefCell<bool>>,
menu: Option<AnyElement<V>>,
}
// Here be dragons
impl<V: 'static> Element<V> for MenuHandle<V> {
type ElementState = MenuState<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 open = if let Some(element_state) = element_state {
element_state.open
} else {
Rc::new(RefCell::new(false))
};
let mut menu = None;
if *open.borrow() {
let mut view = (self.builder)(view_state, cx).render();
child_layout_ids.push(view.layout(view_state, cx));
menu.replace(view);
}
let layout_id = cx.request_layout(&gpui::Style::default(), child_layout_ids.into_iter());
(layout_id, MenuState { open, menu })
}
fn paint(
&mut self,
bounds: crate::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(mut menu) = element_state.menu.as_mut() {
menu.paint(view_state, cx);
return;
}
let open = element_state.open.clone();
cx.on_mouse_event(move |view_state, event: &MouseDownEvent, phase, cx| {
dbg!(&event, &phase);
if phase == DispatchPhase::Bubble
&& event.button == MouseButton::Right
&& bounds.contains_point(&event.position)
{
*open.borrow_mut() = true;
cx.notify();
}
});
}
}
impl<V: 'static> Component<V> for MenuHandle<V> {
fn render(self) -> AnyElement<V> {
AnyElement::new(self)
}
}
struct TestMenu {}
impl Render for TestMenu {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
div().child("0MG!")
}
}
// here be kittens
impl Render for PanelButtons {
type Element = Div<Self>;
@ -807,7 +699,7 @@ impl Render for PanelButtons {
Some(
MenuHandle::new(
SharedString::from(format!("{} tooltip", name)),
move |_, cx| Tooltip::text("HELLOOOOOOOOOOOOOO", cx),
move |_, cx| cx.build_view(|cx| ContextMenu::new(cx).header("SECTION")),
)
.child(button),
)