diff --git a/crates/gpui2/src/elements/overlay.rs b/crates/gpui2/src/elements/overlay.rs index 4d3e8fdbf7..14a8048d39 100644 --- a/crates/gpui2/src/elements/overlay.rs +++ b/crates/gpui2/src/elements/overlay.rs @@ -15,7 +15,7 @@ pub struct Overlay { anchor_corner: AnchorCorner, fit_mode: OverlayFitMode, // todo!(); - // anchor_position: Option, + anchor_position: Option>, // position_mode: OverlayPositionMode, } @@ -26,6 +26,7 @@ pub fn overlay() -> Overlay { children: SmallVec::new(), anchor_corner: AnchorCorner::TopLeft, fit_mode: OverlayFitMode::SwitchAnchor, + anchor_position: None, } } @@ -36,6 +37,13 @@ impl Overlay { self } + /// Sets the position in window co-ordinates + /// (otherwise the location the overlay is rendered is used) + pub fn position(mut self, anchor: Point) -> Self { + self.anchor_position = Some(anchor); + self + } + /// Snap to window edge instead of switching anchor corner when an overflow would occur. pub fn snap_to_window(mut self) -> Self { self.fit_mode = OverlayFitMode::SnapToWindow; @@ -102,7 +110,7 @@ impl Element for Overlay { child_max = child_max.max(&child_bounds.lower_right()); } let size: Size = (child_max - child_min).into(); - let origin = bounds.origin; + let origin = self.anchor_position.unwrap_or(bounds.origin); let mut desired = self.anchor_corner.get_bounds(origin, size); let limits = Bounds { @@ -196,6 +204,15 @@ impl AnchorCorner { Bounds { origin, size } } + pub fn corner(&self, bounds: Bounds) -> Point { + match self { + Self::TopLeft => bounds.origin, + Self::TopRight => bounds.upper_right(), + Self::BottomLeft => bounds.lower_left(), + Self::BottomRight => bounds.lower_right(), + } + } + fn switch_axis(self, axis: Axis) -> Self { match axis { Axis::Vertical => match self { diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 17bd4743b1..b8066d4889 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1151,6 +1151,14 @@ impl<'a> WindowContext<'a> { self.window.mouse_position = mouse_move.position; InputEvent::MouseMove(mouse_move) } + InputEvent::MouseDown(mouse_down) => { + self.window.mouse_position = mouse_down.position; + InputEvent::MouseDown(mouse_down) + } + InputEvent::MouseUp(mouse_up) => { + self.window.mouse_position = mouse_up.position; + InputEvent::MouseUp(mouse_up) + } // Translate dragging and dropping of external files from the operating system // to internal drag and drop events. InputEvent::FileDrop(file_drop) => match file_drop { diff --git a/crates/terminal_view2/src/terminal_view.rs b/crates/terminal_view2/src/terminal_view.rs index 4d77d172a6..80bf65aec4 100644 --- a/crates/terminal_view2/src/terminal_view.rs +++ b/crates/terminal_view2/src/terminal_view.rs @@ -32,7 +32,7 @@ use workspace::{ notifications::NotifyResultExt, register_deserializable_item, searchable::{SearchEvent, SearchOptions, SearchableItem}, - ui::{ContextMenu, ContextMenuItem, Label}, + ui::{ContextMenu, Label}, CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId, }; diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index 5d4974e631..112e1224f9 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -1,56 +1,14 @@ use std::cell::RefCell; use std::rc::Rc; -use crate::{prelude::*, ListItemVariant}; +use crate::prelude::*; use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHeader}; use gpui::{ - overlay, px, Action, AnyElement, Bounds, DispatchPhase, EventEmitter, FocusHandle, - FocusableView, LayoutId, MouseButton, MouseDownEvent, Overlay, Render, View, + overlay, px, Action, AnchorCorner, AnyElement, Bounds, DispatchPhase, Div, EventEmitter, + FocusHandle, FocusableView, LayoutId, MouseButton, MouseDownEvent, Pixels, Point, Render, View, }; use smallvec::SmallVec; -pub enum ContextMenuItem { - Header(SharedString), - Entry(Label, Box), - Separator, -} - -impl Clone for ContextMenuItem { - fn clone(&self) -> Self { - match self { - ContextMenuItem::Header(name) => ContextMenuItem::Header(name.clone()), - ContextMenuItem::Entry(label, action) => { - ContextMenuItem::Entry(label.clone(), action.boxed_clone()) - } - ContextMenuItem::Separator => ContextMenuItem::Separator, - } - } -} -impl ContextMenuItem { - 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) - .action(action) - .into(), - ContextMenuItem::Separator => ListSeparator::new().into(), - } - } - - pub fn header(label: impl Into) -> Self { - Self::Header(label.into()) - } - - pub fn separator() -> Self { - Self::Separator - } - - pub fn entry(label: Label, action: impl Action) -> Self { - Self::Entry(label, Box::new(action)) - } -} - pub struct ContextMenu { items: Vec, focus_handle: FocusHandle, @@ -101,67 +59,93 @@ impl ContextMenu { } impl Render for ContextMenu { - type Element = Overlay; + type Element = Div; // 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())), - ), + 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>, -} + id: Option, + child_builder: Option AnyElement + 'static>>, + menu_builder: Option) -> View + 'static>>, -impl ParentComponent for MenuHandle { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { - &mut self.children - } + anchor: Option, + attach: Option, } impl MenuHandle { - pub fn new( - id: impl Into, - builder: impl Fn(&mut V, &mut ViewContext) -> View + 'static, + pub fn id(mut self, id: impl Into) -> Self { + self.id = Some(id.into()); + self + } + + pub fn menu( + mut self, + f: impl Fn(&mut V, &mut ViewContext) -> View + 'static, ) -> Self { - Self { - id: id.into(), - children: SmallVec::new(), - builder: Rc::new(builder), - } + self.menu_builder = Some(Rc::new(f)); + self + } + + pub fn child>(mut self, f: impl FnOnce(bool) -> R + 'static) -> Self { + self.child_builder = Some(Box::new(|b| f(b).render())); + 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 = Some(anchor); + self + } + + /// 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 + } +} + +pub fn menu_handle() -> MenuHandle { + MenuHandle { + id: None, + child_builder: None, + menu_builder: None, + anchor: None, + attach: None, } } pub struct MenuHandleState { menu: Rc>>>, + position: Rc>>, + child_layout_id: Option, + child_element: Option>, menu_element: Option>, } impl Element for MenuHandle { type ElementState = MenuHandleState; fn element_id(&self) -> Option { - Some(self.id.clone()) + Some(self.id.clone().expect("menu_handle must have an id()")) } fn layout( @@ -170,27 +154,50 @@ impl Element for MenuHandle { 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 + let (menu, position) = if let Some(element_state) = element_state { + (element_state.menu, element_state.position) } else { - Rc::new(RefCell::new(None)) + (Rc::default(), Rc::default()) }; + let mut menu_layout_id = 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)); + let mut overlay = overlay::().snap_to_window(); + if let Some(anchor) = self.anchor { + overlay = overlay.anchor(anchor); + } + overlay = overlay.position(*position.borrow()); + + let mut view = overlay.child(menu.clone()).render(); + menu_layout_id = Some(view.layout(view_state, cx)); view }); - let layout_id = cx.request_layout(&gpui::Style::default(), child_layout_ids.into_iter()); + let mut child_element = self + .child_builder + .take() + .map(|child_builder| (child_builder)(menu.borrow().is_some())); - (layout_id, MenuHandleState { menu, menu_element }) + let child_layout_id = child_element + .as_mut() + .map(|child_element| child_element.layout(view_state, cx)); + + let layout_id = cx.request_layout( + &gpui::Style::default(), + menu_layout_id.into_iter().chain(child_layout_id), + ); + + ( + layout_id, + MenuHandleState { + menu, + position, + child_element, + child_layout_id, + menu_element, + }, + ) } fn paint( @@ -200,7 +207,7 @@ impl Element for MenuHandle { element_state: &mut Self::ElementState, cx: &mut crate::ViewContext, ) { - for child in &mut self.children { + if let Some(child) = element_state.child_element.as_mut() { child.paint(view_state, cx); } @@ -209,8 +216,14 @@ impl Element for MenuHandle { return; } + let Some(builder) = self.menu_builder.clone() else { + return; + }; let menu = element_state.menu.clone(); - let builder = self.builder.clone(); + let position = element_state.position.clone(); + let attach = self.attach.clone(); + let child_layout_id = element_state.child_layout_id.clone(); + cx.on_mouse_event(move |view_state, event: &MouseDownEvent, phase, cx| { if phase == DispatchPhase::Bubble && event.button == MouseButton::Right @@ -229,6 +242,14 @@ impl Element for MenuHandle { }) .detach(); *menu.borrow_mut() = Some(new_menu); + + *position.borrow_mut() = if attach.is_some() && child_layout_id.is_some() { + attach + .unwrap() + .corner(cx.layout_bounds(child_layout_id.unwrap())) + } else { + cx.mouse_position() + }; cx.notify(); } }); @@ -250,35 +271,101 @@ mod stories { use crate::story::Story; use gpui::{action, Div, Render, VisualContext}; + #[action] + struct PrintCurrentDate {} + + fn build_menu(cx: &mut WindowContext, header: impl Into) -> View { + cx.build_view(|cx| { + ContextMenu::new(cx).header(header).separator().entry( + Label::new("Print current time"), + PrintCurrentDate {}.boxed_clone(), + ) + }) + } + pub struct ContextMenuStory; impl Render for ContextMenuStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - #[action] - struct PrintCurrentDate {} - Story::container(cx) - .child(Story::title_for::<_, ContextMenu>(cx)) .on_action(|_, _: &PrintCurrentDate, _| { if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() { println!("Current Unix time is {:?}", unix_time.as_secs()); } }) + .flex() + .flex_row() + .justify_between() .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")), + div() + .flex() + .flex_col() + .justify_between() + .child( + menu_handle() + .id("test2") + .child(|is_open| { + Label::new(if is_open { + "TOP LEFT" + } else { + "RIGHT CLICK ME" + }) + .render() + }) + .menu(move |_, cx| build_menu(cx, "top left")), + ) + .child( + menu_handle() + .id("test1") + .child(|is_open| { + Label::new(if is_open { + "BOTTOM LEFT" + } else { + "RIGHT CLICK ME" + }) + .render() + }) + .anchor(AnchorCorner::BottomLeft) + .attach(AnchorCorner::TopLeft) + .menu(move |_, cx| build_menu(cx, "bottom left")), + ), + ) + .child( + div() + .flex() + .flex_col() + .justify_between() + .child( + menu_handle() + .id("test3") + .child(|is_open| { + Label::new(if is_open { + "TOP RIGHT" + } else { + "RIGHT CLICK ME" + }) + .render() + }) + .anchor(AnchorCorner::TopRight) + .menu(move |_, cx| build_menu(cx, "top right")), + ) + .child( + menu_handle() + .id("test4") + .child(|is_open| { + Label::new(if is_open { + "BOTTOM RIGHT" + } else { + "RIGHT CLICK ME" + }) + .render() + }) + .anchor(AnchorCorner::BottomRight) + .attach(AnchorCorner::TopRight) + .menu(move |_, cx| build_menu(cx, "bottom right")), + ), ) } } diff --git a/crates/ui2/src/components/icon_button.rs b/crates/ui2/src/components/icon_button.rs index 1f7b86badd..2772f10a29 100644 --- a/crates/ui2/src/components/icon_button.rs +++ b/crates/ui2/src/components/icon_button.rs @@ -19,6 +19,7 @@ pub struct IconButton { color: TextColor, variant: ButtonVariant, state: InteractionState, + selected: bool, tooltip: Option) -> AnyView + 'static>>, handlers: IconButtonHandlers, } @@ -31,6 +32,7 @@ impl IconButton { color: TextColor::default(), variant: ButtonVariant::default(), state: InteractionState::default(), + selected: false, tooltip: None, handlers: IconButtonHandlers::default(), } @@ -56,6 +58,11 @@ impl IconButton { self } + pub fn selected(mut self, selected: bool) -> Self { + self.selected = selected; + self + } + pub fn tooltip( mut self, tooltip: impl Fn(&mut V, &mut ViewContext) -> AnyView + 'static, @@ -80,7 +87,7 @@ impl IconButton { _ => self.color, }; - let (bg_color, bg_hover_color, bg_active_color) = match self.variant { + let (mut bg_color, bg_hover_color, bg_active_color) = match self.variant { ButtonVariant::Filled => ( cx.theme().colors().element_background, cx.theme().colors().element_hover, @@ -93,6 +100,10 @@ impl IconButton { ), }; + if self.selected { + bg_color = bg_hover_color; + } + let mut button = h_stack() .id(self.id.clone()) .justify_center() @@ -113,7 +124,9 @@ impl IconButton { } if let Some(tooltip) = self.tooltip.take() { - button = button.tooltip(move |view: &mut V, cx| (tooltip)(view, cx)) + if !self.selected { + button = button.tooltip(move |view: &mut V, cx| (tooltip)(view, cx)) + } } button diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index 409385cafc..3a3cc4d490 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -1,18 +1,18 @@ use crate::{status_bar::StatusItemView, Axis, Workspace}; use gpui::{ - div, overlay, point, px, Action, AnyElement, AnyView, AppContext, Component, DispatchPhase, - Div, Element, ElementId, Entity, EntityId, EventEmitter, FocusHandle, FocusableView, - InteractiveComponent, LayoutId, MouseButton, MouseDownEvent, ParentComponent, Pixels, Point, - Render, SharedString, Style, Styled, Subscription, View, ViewContext, VisualContext, WeakView, - WindowContext, + div, overlay, point, px, Action, AnchorCorner, AnyElement, AnyView, AppContext, Component, + DispatchPhase, Div, Element, ElementId, Entity, EntityId, EventEmitter, FocusHandle, + FocusableView, InteractiveComponent, LayoutId, MouseButton, MouseDownEvent, ParentComponent, + Pixels, Point, Render, SharedString, Style, Styled, Subscription, View, ViewContext, + VisualContext, WeakView, WindowContext, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use std::{cell::RefCell, rc::Rc, sync::Arc}; use ui::{ - h_stack, ContextMenu, ContextMenuItem, IconButton, InteractionState, Label, MenuEvent, - MenuHandle, Tooltip, + h_stack, menu_handle, ContextMenu, IconButton, InteractionState, Label, MenuEvent, MenuHandle, + Tooltip, }; pub enum PanelEvent { @@ -672,6 +672,13 @@ impl Render for PanelButtons { let active_index = dock.active_panel_index; let is_open = dock.is_open; + let (menu_anchor, menu_attach) = match dock.position { + DockPosition::Left => (AnchorCorner::BottomLeft, AnchorCorner::TopLeft), + DockPosition::Bottom | DockPosition::Right => { + (AnchorCorner::BottomRight, AnchorCorner::TopRight) + } + }; + let buttons = dock .panel_entries .iter() @@ -697,11 +704,14 @@ impl Render for PanelButtons { }; Some( - MenuHandle::new( - SharedString::from(format!("{} tooltip", name)), - move |_, cx| cx.build_view(|cx| ContextMenu::new(cx).header("SECTION")), - ) - .child(button), + menu_handle() + .id(name) + .menu(move |_, cx| { + cx.build_view(|cx| ContextMenu::new(cx).header("SECTION")) + }) + .anchor(menu_anchor) + .attach(menu_attach) + .child(|is_open| button.selected(is_open)), ) });