Detailed changes
@@ -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(),
}
}
+
+ 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!()
- // 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)))
+ 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),
)