mirror of
https://github.com/zed-industries/zed.git
synced 2025-02-07 02:57:34 +00:00
Progress on ContextMenu
This commit is contained in:
parent
9456f716c2
commit
074a221e0f
5 changed files with 247 additions and 158 deletions
|
@ -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 => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue