diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index 6af63301b9..e4ca428678 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -38,8 +38,8 @@ use gpui::{ use project::{Project, RepositoryEntry}; use theme::ActiveTheme; use ui::{ - h_stack, prelude::*, Avatar, Button, ButtonLike, ButtonStyle2, Icon, IconButton, IconElement, - KeyBinding, PopoverMenu, Tooltip, + h_stack, popover_menu, prelude::*, Avatar, Button, ButtonLike, ButtonStyle2, ContextMenu, Icon, + IconButton, IconElement, KeyBinding, Tooltip, }; use util::ResultExt; use workspace::{notifications::NotifyResultExt, Workspace}; @@ -290,19 +290,20 @@ impl Render for CollabTitlebarItem { // TODO: Finish implementing user menu popover // this.child( - PopoverMenu::new( - ButtonLike::new("user-menu") - .child(h_stack().gap_0p5().child(Avatar::data(avatar)).child( - IconElement::new(Icon::ChevronDown).color(Color::Muted), - )) - .style(ButtonStyle2::Subtle) - .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)) - .into_any_element(), - ) - .anchor(gpui::AnchorCorner::TopRight) - // TODO: Show when trigger is clicked - .show_menu(true) - .children(vec![div().w_96().h_96().bg(gpui::red())]), + popover_menu("user-menu") + .menu(|cx| ContextMenu::build(cx, |menu, cx| menu.header("ADADA"))) + .trigger( + ButtonLike::new("user-menu") + .child( + h_stack().gap_0p5().child(Avatar::data(avatar)).child( + IconElement::new(Icon::ChevronDown) + .color(Color::Muted), + ), + ) + .style(ButtonStyle2::Subtle) + .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)), + ) + .anchor(gpui::AnchorCorner::TopRight), ) // this.child( // ButtonLike::new("user-menu") diff --git a/crates/ui2/src/components/button/button_like.rs b/crates/ui2/src/components/button/button_like.rs index 3c36feb59f..6885ac86e1 100644 --- a/crates/ui2/src/components/button/button_like.rs +++ b/crates/ui2/src/components/button/button_like.rs @@ -261,7 +261,11 @@ impl RenderOnce for ButtonLike { |this, on_click| this.on_click(move |event, cx| (on_click)(event, cx)), ) .when_some(self.tooltip, |this, tooltip| { - this.tooltip(move |cx| tooltip(cx)) + if !self.selected { + this.tooltip(move |cx| tooltip(cx)) + } else { + this + } }) .children(self.children) } diff --git a/crates/ui2/src/components/popover_menu.rs b/crates/ui2/src/components/popover_menu.rs index 9e393ffb34..4b5144e7c7 100644 --- a/crates/ui2/src/components/popover_menu.rs +++ b/crates/ui2/src/components/popover_menu.rs @@ -1,96 +1,231 @@ +use std::{cell::RefCell, rc::Rc}; + use gpui::{ - div, overlay, rems, AnchorCorner, AnyElement, Div, ParentElement, RenderOnce, Styled, - WindowContext, + overlay, point, px, rems, AnchorCorner, AnyElement, Bounds, DismissEvent, DispatchPhase, + Element, ElementId, InteractiveBounds, IntoElement, LayoutId, ManagedView, MouseDownEvent, + ParentElement, Pixels, Point, View, VisualContext, WindowContext, }; -use smallvec::SmallVec; -use crate::{prelude::*, Popover}; +use crate::{Clickable, Selectable}; -#[derive(IntoElement)] -pub struct PopoverMenu { - /// The element that triggers the popover menu when clicked - /// Usually a button - trigger: AnyElement, - /// The content of the popover menu - /// This will automatically be wrapped in a [Popover] element - children: SmallVec<[AnyElement; 2]>, - /// The direction the popover menu will open by default - /// - /// When not enough space is available in the default direction, - /// the popover menu will follow the rules of [gpui2::elements::overlay] +pub trait PopoverTrigger: IntoElement + Clickable + Selectable + 'static {} + +impl PopoverTrigger for T {} + +pub struct PopoverMenu { + id: ElementId, + child_builder: Option< + Box< + dyn FnOnce( + Rc>>>, + Option View + 'static>>, + ) -> AnyElement + + 'static, + >, + >, + menu_builder: Option View + 'static>>, anchor: AnchorCorner, - /// Whether the popover menu is currently open - show_menu: bool, + attach: Option, + offset: Option>, } -impl RenderOnce for PopoverMenu { - type Rendered = Div; - - fn render(self, _cx: &mut WindowContext) -> Self::Rendered { - // Default offset = 4px padding + 1px border - let offset = 5. / 16.; - - let (top, right, bottom, left) = match self.anchor { - AnchorCorner::TopRight => (None, Some(-offset), Some(-offset), None), - AnchorCorner::TopLeft => (None, None, Some(-offset), Some(-offset)), - AnchorCorner::BottomRight => (Some(-offset), Some(-offset), None, None), - AnchorCorner::BottomLeft => (Some(-offset), None, None, Some(-offset)), - }; - - div() - .flex() - .flex_none() - .bg(gpui::green()) - .relative() - .child( - div() - .flex_none() - .relative() - .bg(gpui::blue()) - .child(self.trigger), - ) - .when(self.show_menu, |this| { - this.child( - div() - .absolute() - .size_0() - .when_some(top, |this, t| this.top(rems(t))) - .when_some(right, |this, r| this.right(rems(r))) - .when_some(bottom, |this, b| this.bottom(rems(b))) - .when_some(left, |this, l| this.left(rems(l))) - .child( - overlay() - .anchor(AnchorCorner::TopRight) - .child(Popover::new().children(self.children)), - ), - ) - }) - } -} - -impl PopoverMenu { - pub fn new(trigger: AnyElement) -> Self { - Self { - trigger, - children: SmallVec::new(), - anchor: AnchorCorner::TopLeft, - show_menu: false, - } +impl PopoverMenu { + pub fn menu(mut self, f: impl Fn(&mut WindowContext) -> View + 'static) -> Self { + self.menu_builder = Some(Rc::new(f)); + self } + pub fn trigger(mut self, t: T) -> Self { + self.child_builder = Some(Box::new(|menu, builder| { + let open = menu.borrow().is_some(); + t.selected(open) + .when_some(builder, |el, builder| { + el.on_click({ + move |_, cx| { + let new_menu = (builder)(cx); + let menu2 = menu.clone(); + let previous_focus_handle = cx.focused(); + + cx.subscribe(&new_menu, move |modal, _: &DismissEvent, cx| { + if modal.focus_handle(cx).contains_focused(cx) { + if previous_focus_handle.is_some() { + cx.focus(&previous_focus_handle.as_ref().unwrap()) + } + } + *menu2.borrow_mut() = None; + cx.notify(); + }) + .detach(); + cx.focus_view(&new_menu); + *menu.borrow_mut() = Some(new_menu); + } + }) + }) + .into_any_element() + })); + self + } + + /// anchor defines which corner of the menu to anchor to the attachment point + /// (by default the cursor position, but see attach) pub fn anchor(mut self, anchor: AnchorCorner) -> Self { self.anchor = anchor; self } - pub fn show_menu(mut self, show_menu: bool) -> Self { - self.show_menu = show_menu; + /// attach defines which corner of the handle to attach the menu's anchor to + pub fn attach(mut self, attach: AnchorCorner) -> Self { + self.attach = Some(attach); self } + + /// offset offsets the position of the content by that many pixels. + pub fn offset(mut self, offset: Point) -> Self { + self.offset = Some(offset); + self + } + + fn resolved_attach(&self) -> AnchorCorner { + self.attach.unwrap_or_else(|| match self.anchor { + AnchorCorner::TopLeft => AnchorCorner::BottomLeft, + AnchorCorner::TopRight => AnchorCorner::BottomRight, + AnchorCorner::BottomLeft => AnchorCorner::TopLeft, + AnchorCorner::BottomRight => AnchorCorner::TopRight, + }) + } + + fn resolved_offset(&self, cx: &WindowContext) -> Point { + self.offset.unwrap_or_else(|| { + // Default offset = 4px padding + 1px border + let offset = rems(5. / 16.) * cx.rem_size(); + match self.anchor { + AnchorCorner::TopRight | AnchorCorner::BottomRight => point(offset, px(0.)), + AnchorCorner::TopLeft | AnchorCorner::BottomLeft => point(-offset, px(0.)), + } + }) + } } -impl ParentElement for PopoverMenu { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { - &mut self.children +pub fn popover_menu(id: impl Into) -> PopoverMenu { + PopoverMenu { + id: id.into(), + child_builder: None, + menu_builder: None, + anchor: AnchorCorner::TopLeft, + attach: None, + offset: None, + } +} + +pub struct PopoverMenuState { + child_layout_id: Option, + child_element: Option, + child_bounds: Option>, + menu_element: Option, + menu: Rc>>>, +} + +impl Element for PopoverMenu { + type State = PopoverMenuState; + + fn layout( + &mut self, + element_state: Option, + cx: &mut WindowContext, + ) -> (gpui::LayoutId, Self::State) { + let mut menu_layout_id = None; + + let (menu, child_bounds) = if let Some(element_state) = element_state { + (element_state.menu, element_state.child_bounds) + } else { + (Rc::default(), None) + }; + + let menu_element = menu.borrow_mut().as_mut().map(|menu| { + let mut overlay = overlay().snap_to_window().anchor(self.anchor); + + if let Some(child_bounds) = child_bounds { + overlay = overlay.position( + self.resolved_attach().corner(child_bounds) + self.resolved_offset(cx), + ); + } + + let mut element = overlay.child(menu.clone()).into_any(); + menu_layout_id = Some(element.layout(cx)); + element + }); + + let mut child_element = self + .child_builder + .take() + .map(|child_builder| (child_builder)(menu.clone(), self.menu_builder.clone())); + + let child_layout_id = child_element + .as_mut() + .map(|child_element| child_element.layout(cx)); + + let layout_id = cx.request_layout( + &gpui::Style::default(), + menu_layout_id.into_iter().chain(child_layout_id), + ); + + ( + layout_id, + PopoverMenuState { + menu, + child_element, + child_layout_id, + menu_element, + child_bounds, + }, + ) + } + + fn paint( + self, + _: Bounds, + element_state: &mut Self::State, + cx: &mut WindowContext, + ) { + if let Some(child) = element_state.child_element.take() { + child.paint(cx); + } + + if let Some(child_layout_id) = element_state.child_layout_id.take() { + element_state.child_bounds = Some(cx.layout_bounds(child_layout_id)); + } + + if let Some(menu) = element_state.menu_element.take() { + menu.paint(cx); + + if let Some(child_bounds) = element_state.child_bounds { + let interactive_bounds = InteractiveBounds { + bounds: child_bounds, + stacking_order: cx.stacking_order().clone(), + }; + + // Mouse-downing outside the menu dismisses it, so we don't + // want a click on the toggle to re-open it. + cx.on_mouse_event(move |e: &MouseDownEvent, phase, cx| { + if phase == DispatchPhase::Bubble + && interactive_bounds.visibly_contains(&e.position, cx) + { + cx.stop_propagation() + } + }) + } + } + } +} + +impl IntoElement for PopoverMenu { + type Element = Self; + + fn element_id(&self) -> Option { + Some(self.id.clone()) + } + + fn into_element(self) -> Self::Element { + self } }