diff --git a/crates/ui/src/input/input.rs b/crates/ui/src/input/input.rs index 032c56e06b..fe0b4bdca7 100644 --- a/crates/ui/src/input/input.rs +++ b/crates/ui/src/input/input.rs @@ -2,14 +2,14 @@ use std::rc::Rc; use gpui::prelude::FluentBuilder as _; use gpui::{ - AnyElement, App, Context, DefiniteLength, Edges, EdgesRefinement, Entity, Hsla, + AnyElement, App, DefiniteLength, Edges, EdgesRefinement, Entity, Hsla, InteractiveElement as _, IntoElement, MouseButton, ParentElement as _, Rems, RenderOnce, StyleRefinement, Styled, TextAlign, Window, div, px, relative, }; use crate::button::{Button, ButtonVariants as _}; use crate::input::clear_button; -use crate::menu::PopupMenu; +use crate::native_menu::NativeMenu; use crate::spinner::Spinner; use crate::{ActiveTheme, Colorize, v_flex}; use crate::{IconName, Size}; @@ -50,9 +50,8 @@ pub struct Input { /// An optional context menu builder to allow a custom context menu on the input. /// - /// If set, this will override the built-in context menu. - context_menu_builder: - Option) -> PopupMenu>>, + /// If set, this overrides the built-in context menu. + context_menu_builder: Option NativeMenu>>, } impl Sizable for Input { @@ -159,10 +158,12 @@ impl Input { self } - /// Sets the context menu for the input. + /// Sets a custom context menu builder for the input, shown as a native OS menu. + /// + /// If set, this overrides the built-in right-click context menu. pub fn context_menu( mut self, - f: impl Fn(PopupMenu, &mut Window, &mut Context) -> PopupMenu + 'static, + f: impl Fn(NativeMenu, &mut Window, &mut App) -> NativeMenu + 'static, ) -> Self { self.context_menu_builder = Some(Rc::new(f)); self diff --git a/crates/ui/src/input/lsp/mod.rs b/crates/ui/src/input/lsp/mod.rs index e77e273c15..b7ce1b54ba 100644 --- a/crates/ui/src/input/lsp/mod.rs +++ b/crates/ui/src/input/lsp/mod.rs @@ -127,7 +127,6 @@ impl InputState { handled = menu.handle_action(action, window, cx) }); } - ContextMenu::RightClick(..) => {} }; handled diff --git a/crates/ui/src/input/popovers/context_menu.rs b/crates/ui/src/input/popovers/context_menu.rs deleted file mode 100644 index 565f61f1d2..0000000000 --- a/crates/ui/src/input/popovers/context_menu.rs +++ /dev/null @@ -1,160 +0,0 @@ -use gpui::{ - Anchor, App, AppContext as _, Context, DismissEvent, Entity, IntoElement, MouseDownEvent, - ParentElement as _, Pixels, Point, Render, Styled, Subscription, Window, anchored, deferred, - div, prelude::FluentBuilder as _, px, -}; -use rust_i18n::t; - -use crate::{ - ActiveTheme as _, - global_state::GlobalState, - input::{self, InputState, popovers::ContextMenu}, - menu::PopupMenu, -}; - -/// Context menu for mouse right clicks. -pub(crate) struct InputContextMenu { - editor: Entity, - menu: Entity, - mouse_position: Point, - open: bool, - - _subscriptions: Vec, -} - -impl InputState { - pub(crate) fn handle_right_click_menu( - &mut self, - event: &MouseDownEvent, - offset: usize, - window: &mut Window, - cx: &mut Context, - ) { - // Check if we are already in a deferred context (e.g., inside a Popover) - // If so, don't show the context menu to prevent double-deferred panic - if GlobalState::global(cx).is_in_deferred_context() { - return; - } - - // Show Mouse context menu - if !self.selected_range.contains(offset) { - self.move_to(offset, None, cx); - } - - self.context_menu_content = Some(ContextMenu::RightClick(self.context_menu.clone())); - - let is_code_editor = self.mode.is_code_editor(); - if is_code_editor { - self.handle_hover_definition(offset, window, cx); - } - - let is_enable = !self.disabled; - let has_goto_definition = is_enable && self.lsp.definition_provider.is_some(); - let has_code_action = is_enable && !self.lsp.code_action_providers.is_empty(); - let is_selected = !self.selected_range.is_empty(); - let has_paste = is_enable && cx.read_from_clipboard().is_some(); - - let action_context = self.focus_handle.clone(); - self.context_menu.update(cx, |this, cx| { - this.mouse_position = event.position; - this.menu.update(cx, |menu, cx| { - let new_menu = if let Some(builder) = &self.context_menu_builder { - builder(PopupMenu::new(cx), window, cx) - } else { - PopupMenu::new(cx) - .when(is_code_editor, |m| { - m.menu_with_enable( - t!("Input.Go to Definition"), - Box::new(input::GoToDefinition), - has_goto_definition, - ) - .menu_with_enable( - t!("Input.Show Code Actions"), - Box::new(input::ToggleCodeActions), - has_code_action, - ) - .separator() - }) - .menu_with_enable( - t!("Input.Cut"), - Box::new(input::Cut), - is_enable && is_selected, - ) - .menu_with_enable(t!("Input.Copy"), Box::new(input::Copy), is_selected) - .menu_with_enable(t!("Input.Paste"), Box::new(input::Paste), has_paste) - .separator() - .menu(t!("Input.Select All"), Box::new(input::SelectAll)) - }; - - menu.menu_items = new_menu.menu_items; - menu.action_context = Some(action_context); - cx.notify(); - }); - cx.defer_in(window, |this, _, cx| { - this.open = true; - cx.notify(); - }); - }); - } -} - -impl InputContextMenu { - pub(crate) fn new( - editor: Entity, - window: &mut Window, - cx: &mut App, - ) -> Entity { - cx.new(|cx| { - let menu = cx.new(|cx| PopupMenu::new(cx).small()); - - let _subscriptions = vec![cx.subscribe_in(&menu, window, { - move |this: &mut Self, _, _: &DismissEvent, window, cx| { - this.close(window, cx); - } - })]; - - Self { - editor, - menu, - mouse_position: Point::default(), - open: false, - _subscriptions, - } - }) - } - - #[inline] - pub(crate) fn is_open(&self) -> bool { - self.open - } - - #[inline] - pub(crate) fn close(&mut self, window: &mut Window, cx: &mut Context) { - self.open = false; - self.editor.update(cx, |this, cx| { - this.focus(window, cx); - }); - } -} - -impl Render for InputContextMenu { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - if !self.open { - return div().into_any_element(); - } - - deferred( - anchored() - .snap_to_window_with_margin(px(8.)) - .anchor(Anchor::TopLeft) - .position(self.mouse_position) - .child( - div() - .font_family(cx.theme().font_family.clone()) - .cursor_default() - .child(self.menu.clone()), - ), - ) - .into_any_element() - } -} diff --git a/crates/ui/src/input/popovers/mod.rs b/crates/ui/src/input/popovers/mod.rs index 07360b8774..2d9013c0ff 100644 --- a/crates/ui/src/input/popovers/mod.rs +++ b/crates/ui/src/input/popovers/mod.rs @@ -1,12 +1,10 @@ mod code_action_menu; mod completion_menu; -mod context_menu; mod diagnostic_popover; mod hover_popover; pub(crate) use code_action_menu::*; pub(crate) use completion_menu::*; -pub(crate) use context_menu::*; pub(crate) use diagnostic_popover::*; pub(crate) use hover_popover::*; @@ -23,7 +21,6 @@ use crate::{ pub(crate) enum ContextMenu { Completion(Entity), CodeAction(Entity), - RightClick(Entity), } impl ContextMenu { @@ -31,7 +28,6 @@ impl ContextMenu { match self { ContextMenu::Completion(menu) => menu.read(cx).is_open(), ContextMenu::CodeAction(menu) => menu.read(cx).is_open(), - ContextMenu::RightClick(menu) => menu.read(cx).is_open(), } } @@ -39,7 +35,6 @@ impl ContextMenu { match self { ContextMenu::Completion(menu) => menu.clone().into_any_element(), ContextMenu::CodeAction(menu) => menu.clone().into_any_element(), - ContextMenu::RightClick(menu) => menu.clone().into_any_element(), } } } diff --git a/crates/ui/src/input/state.rs b/crates/ui/src/input/state.rs index 64bd1accda..6f09fb5b51 100644 --- a/crates/ui/src/input/state.rs +++ b/crates/ui/src/input/state.rs @@ -40,10 +40,10 @@ use crate::input::{ HoverDefinition, InlineCompletion, Lsp, Position, RopeExt as _, Selection, display_map::LineLayout, element::RIGHT_MARGIN, - popovers::{ContextMenu, DiagnosticPopover, HoverPopover, InputContextMenu}, + popovers::{ContextMenu, DiagnosticPopover, HoverPopover}, search::SearchPanel, }; -use crate::menu::PopupMenu; +use crate::native_menu::NativeMenu; use crate::scroll::AutoScroll; use crate::{Root, history::History}; @@ -396,17 +396,16 @@ pub struct InputState { diagnostic_popover: Option>, /// Completion/CodeAction context menu pub(super) context_menu_content: Option, - pub(super) context_menu: Entity, - /// An optional context menu builder to allow a custom context menu on the input. + /// An optional context menu builder to allow a custom right-click context menu on the input. /// - /// If set, this will override the built-in context menu and ignore the value set in [`Self::enable_context_menu`]. + /// If set, this overrides the built-in context menu (and ignores [`Self::enable_context_menu`]). pub(super) context_menu_builder: - Option) -> PopupMenu>>, + Option NativeMenu>>, /// Whether the context menu that shows on right-click is enabled. /// - /// This value will be ignored if a context menu builder is defined in [`Self::context_menu_builder`]. + /// This value is ignored if a context menu builder is defined in [`Self::context_menu_builder`]. pub(super) enable_context_menu: bool, /// A flag to indicate if we are currently inserting a completion item. @@ -469,7 +468,6 @@ impl InputState { ]; let text_style = window.text_style(); - let mouse_context_menu = InputContextMenu::new(cx.entity(), window, cx); Self { focus_handle: focus_handle.clone(), @@ -519,7 +517,6 @@ impl InputState { lsp: Lsp::default(), diagnostic_popover: None, context_menu_content: None, - context_menu: mouse_context_menu, context_menu_builder: None, enable_context_menu: true, completion_inserting: false, @@ -582,7 +579,7 @@ impl InputState { /// Sets whether the context menu that shows on right-click is enabled. /// /// The context menu is enabled by default. - /// This value will be ignored if a custom context menu is defined on the input. + /// This value is ignored if a custom context menu builder is defined on the input. pub fn context_menu(mut self, enable: bool) -> Self { self.enable_context_menu = enable; self @@ -1503,6 +1500,78 @@ impl InputState { cx.propagate(); } + /// Show the right-click context menu as a native OS menu. + pub(crate) fn handle_right_click_menu( + &mut self, + event: &MouseDownEvent, + offset: usize, + window: &mut Window, + cx: &mut Context, + ) { + if crate::global_state::GlobalState::global(cx).is_in_deferred_context() { + return; + } + + if !self.selected_range.contains(offset) { + self.move_to(offset, None, cx); + } + + // A custom builder fully replaces the built-in context menu. + let menu = if let Some(builder) = self.context_menu_builder.clone() { + builder(NativeMenu::new(), window, cx) + } else { + let is_code_editor = self.mode.is_code_editor(); + if is_code_editor { + self.handle_hover_definition(offset, window, cx); + } + + let is_enable = !self.disabled; + let has_goto_definition = is_enable && self.lsp.definition_provider.is_some(); + let has_code_action = is_enable && !self.lsp.code_action_providers.is_empty(); + let is_selected = !self.selected_range.is_empty(); + let has_paste = is_enable && cx.read_from_clipboard().is_some(); + + let mut menu = NativeMenu::new(); + if is_code_editor { + menu = menu + .menu_with_disabled( + rust_i18n::t!("Input.Go to Definition"), + !has_goto_definition, + Box::new(crate::input::GoToDefinition), + ) + .menu_with_disabled( + rust_i18n::t!("Input.Show Code Actions"), + !has_code_action, + Box::new(crate::input::ToggleCodeActions), + ) + .separator(); + } + + menu.menu_with_disabled( + rust_i18n::t!("Input.Cut"), + !(is_enable && is_selected), + Box::new(crate::input::Cut), + ) + .menu_with_disabled( + rust_i18n::t!("Input.Copy"), + !is_selected, + Box::new(crate::input::Copy), + ) + .menu_with_disabled( + rust_i18n::t!("Input.Paste"), + !has_paste, + Box::new(crate::input::Paste), + ) + .separator() + .menu( + rust_i18n::t!("Input.Select All"), + Box::new(crate::input::SelectAll), + ) + }; + + menu.show(event.position, window, cx); + } + pub(super) fn on_mouse_down( &mut self, event: &MouseDownEvent, diff --git a/crates/ui/src/menu/popup_menu.rs b/crates/ui/src/menu/popup_menu.rs index bba2100e62..78d32b8903 100644 --- a/crates/ui/src/menu/popup_menu.rs +++ b/crates/ui/src/menu/popup_menu.rs @@ -656,12 +656,6 @@ impl PopupMenu { self } - /// Use small size, the menu item will have smaller height. - pub(crate) fn small(mut self) -> Self { - self.size = Size::Small; - self - } - fn add_menu_item( &mut self, label: impl Into, diff --git a/crates/ui/src/native_menu/macos.rs b/crates/ui/src/native_menu/macos.rs index 0c22758dd5..93fe86da1b 100644 --- a/crates/ui/src/native_menu/macos.rs +++ b/crates/ui/src/native_menu/macos.rs @@ -60,12 +60,18 @@ pub(super) fn show( let handle = Window::window_handle(window); cx.spawn(async move |cx| { - let Some(action) = run_menu(view_ptr, &items, position) else { - return; - }; - cx.update(move |app| { + let action = run_menu(view_ptr, &items, position); + let _ = cx.update(move |app| { let _ = handle.update(app, move |_, window, app| { - window.dispatch_action(action, app); + if let Some(action) = action { + window.dispatch_action(action, app); + } + // Wake GPUI after the AppKit tracking loop returns so the window + // resumes painting and re-registers its mouse handlers. Without + // this, a dismissed menu (especially when nothing is selected) + // leaves the window idle and unresponsive to a second + // right-click. + window.refresh(); }); }); })