From 074a221e0f6a25603ba02ac24c9e5ac347436c5c Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 16 Nov 2023 16:59:27 -0700 Subject: [PATCH] Progress on ContextMenu --- crates/gpui2/src/elements/overlay.rs | 9 +- crates/terminal_view2/src/terminal_view.rs | 14 +- crates/ui2/src/components/context_menu.rs | 240 ++++++++++++++++++--- crates/ui2/src/components/list.rs | 24 ++- crates/workspace2/src/dock.rs | 118 +--------- 5 files changed, 247 insertions(+), 158 deletions(-) diff --git a/crates/gpui2/src/elements/overlay.rs b/crates/gpui2/src/elements/overlay.rs index 69ac9c50dc..8580ae3eb0 100644 --- a/crates/gpui2/src/elements/overlay.rs +++ b/crates/gpui2/src/elements/overlay.rs @@ -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 Element for Overlay { .iter_mut() .map(|child| child.layout(view_state, cx)) .collect::>(); + 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 Element for Overlay { origin: Point::zero(), size: cx.viewport_size(), }; + dbg!(bounds, desired, limits); match self.fit_mode { OverlayFitMode::SnapToWindow => { diff --git a/crates/terminal_view2/src/terminal_view.rs b/crates/terminal_view2/src/terminal_view.rs index 14391ca2b2..4d77d172a6 100644 --- a/crates/terminal_view2/src/terminal_view.rs +++ b/crates/terminal_view2/src/terminal_view.rs @@ -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, + context_menu: Option>, blink_state: bool, blinking_on: bool, blinking_paused: bool, @@ -302,10 +302,14 @@ impl TerminalView { position: gpui::Point, cx: &mut ViewContext, ) { - 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 diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index 8f32c3ed56..8024d334b5 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -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(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, + items: Vec, + focus_handle: FocusHandle, +} + +pub enum MenuEvent { + Dismissed, +} + +impl EventEmitter 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) -> 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(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - 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::) - .collect(), - )) - .on_mouse_down_out(|_, _, cx| cx.dispatch_action(Box::new(menu::Cancel))) + + pub fn header(mut self, title: impl Into) -> 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) -> Self { + self.items.push(ListEntry::new(label).action(action).into()); + self + } + + pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { + // todo!() + cx.emit(MenuEvent::Dismissed); + } + + pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { + cx.emit(MenuEvent::Dismissed); + } +} + +impl Render for ContextMenu { + type Element = Overlay; + // todo!() + fn render(&mut self, cx: &mut ViewContext) -> 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 { + id: ElementId, + children: SmallVec<[AnyElement; 2]>, + builder: Rc) -> View + 'static>, +} + +impl ParentComponent for MenuHandle { + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { + &mut self.children + } +} + +impl MenuHandle { + pub fn new( + id: impl Into, + builder: impl Fn(&mut V, &mut ViewContext) -> View + 'static, + ) -> Self { + Self { + id: id.into(), + children: SmallVec::new(), + builder: Rc::new(builder), + } + } +} + +pub struct MenuHandleState { + menu: Rc>>>, + menu_element: Option>, +} +impl Element for MenuHandle { + type ElementState = MenuHandleState; + + fn element_id(&self) -> Option { + Some(self.id.clone()) + } + + fn layout( + &mut self, + view_state: &mut V, + element_state: Option, + cx: &mut crate::ViewContext, + ) -> (gpui::LayoutId, Self::ElementState) { + let mut child_layout_ids = self + .children + .iter_mut() + .map(|child| child.layout(view_state, cx)) + .collect::>(); + + 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, + view_state: &mut V, + element_state: &mut Self::ElementState, + cx: &mut crate::ViewContext, + ) { + 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 Component for MenuHandle { + fn render(self) -> AnyElement { + 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")), + ) } } } diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index 4b355dd5b6..b9508c5413 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -117,7 +117,7 @@ impl ListHeader { } } -#[derive(Component)] +#[derive(Component, Clone)] pub struct ListSubHeader { label: SharedString, left_icon: Option, @@ -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>, } +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>) -> Self { + pub fn action(mut self, action: impl Into>) -> Self { self.on_click = Some(action.into()); self } diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index f2dd2c15cf..409385cafc 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -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 { - id: ElementId, - children: SmallVec<[AnyElement; 2]>, - builder: Rc) -> AnyView + 'static>, -} - -impl ParentComponent for MenuHandle { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { - &mut self.children - } -} - -impl MenuHandle { - fn new( - id: impl Into, - builder: impl Fn(&mut V, &mut ViewContext) -> AnyView + 'static, - ) -> Self { - Self { - id: id.into(), - children: SmallVec::new(), - builder: Rc::new(builder), - } - } -} - -pub struct MenuState { - open: Rc>, - menu: Option>, -} -// Here be dragons -impl Element for MenuHandle { - type ElementState = MenuState; - - fn element_id(&self) -> Option { - Some(self.id.clone()) - } - - fn layout( - &mut self, - view_state: &mut V, - element_state: Option, - cx: &mut crate::ViewContext, - ) -> (gpui::LayoutId, Self::ElementState) { - let mut child_layout_ids = self - .children - .iter_mut() - .map(|child| child.layout(view_state, cx)) - .collect::>(); - - 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, - view_state: &mut V, - element_state: &mut Self::ElementState, - cx: &mut crate::ViewContext, - ) { - 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 Component for MenuHandle { - fn render(self) -> AnyElement { - AnyElement::new(self) - } -} - -struct TestMenu {} -impl Render for TestMenu { - type Element = Div; - - fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - div().child("0MG!") - } -} - // here be kittens impl Render for PanelButtons { type Element = Div; @@ -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), )