input: Add context menu for cut, copy, paste. (#1279)

Close #27

<img width="756" height="674" alt="image"
src="https://github.com/user-attachments/assets/07884a7e-366b-47b8-8bd7-a88f3cae52b8"
/>
This commit is contained in:
Jason Lee 2025-09-23 22:06:58 +08:00 committed by GitHub
parent 993ce34bd8
commit bf5650b5db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 342 additions and 71 deletions

View file

@ -160,9 +160,31 @@ Input:
en: Replace en: Replace
zh-CN: 替换 zh-CN: 替换
zh-HK: 替換 zh-HK: 替換
it: Sostituisci
Replace All: Replace All:
en: Replace All en: Replace All
zh-CN: 全部替换 zh-CN: 全部替换
zh-HK: 全部替換 zh-HK: 全部替換
it: Sostituisci tutto Cut:
en: Cut
zh-CN: 剪切
zh-HK: 剪切
Copy:
en: Copy
zh-CN: 复制
zh-HK: 複製
Paste:
en: Paste
zh-CN: 粘贴
zh-HK: 貼上
Select All:
en: Select All
zh-CN: 全选
zh-HK: 全選
Go to Definition:
en: Go to Definition
zh-CN: 跳转到定义
zh-HK: 跳轉到定義
Show Code Actions:
en: Show Code Actions
zh-CN: 显示代码操作
zh-HK: 顯示代碼操作

View file

@ -25,6 +25,11 @@ impl Selection {
self.start = 0; self.start = 0;
self.end = 0; self.end = 0;
} }
/// Checks if the given offset is within the selection range.
pub fn contains(&self, offset: usize) -> bool {
offset >= self.start && offset < self.end
}
} }
impl From<Range<usize>> for Selection { impl From<Range<usize>> for Selection {

View file

@ -5,7 +5,7 @@ use std::ops::Range;
use crate::input::{ use crate::input::{
popovers::{CodeActionItem, CodeActionMenu, ContextMenu}, popovers::{CodeActionItem, CodeActionMenu, ContextMenu},
InputState, InputState, ToggleCodeActions,
}; };
pub trait CodeActionProvider { pub trait CodeActionProvider {
@ -37,6 +37,15 @@ pub trait CodeActionProvider {
} }
impl InputState { impl InputState {
pub(crate) fn on_action_toggle_code_actions(
&mut self,
_: &ToggleCodeActions,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.handle_code_action_trigger(window, cx)
}
/// Show code actions for the cursor. /// Show code actions for the cursor.
pub(crate) fn handle_code_action_trigger( pub(crate) fn handle_code_action_trigger(
&mut self, &mut self,

View file

@ -6,7 +6,7 @@ use rope::Rope;
use std::{ops::Range, rc::Rc}; use std::{ops::Range, rc::Rc};
use crate::{ use crate::{
input::{element::TextElement, InputState, RopeExt}, input::{element::TextElement, GoToDefinition, InputState, RopeExt},
ActiveTheme, ActiveTheme,
}; };
@ -31,14 +31,31 @@ pub(crate) struct HoverDefinition {
/// The range of the symbol that triggered the hover. /// The range of the symbol that triggered the hover.
symbol_range: Range<usize>, symbol_range: Range<usize>,
pub(crate) locations: Rc<Vec<lsp_types::LocationLink>>, pub(crate) locations: Rc<Vec<lsp_types::LocationLink>>,
last_location: Option<(Range<usize>, Rc<Vec<lsp_types::LocationLink>>)>,
} }
impl HoverDefinition { impl HoverDefinition {
pub(crate) fn new(symbol_range: Range<usize>, locations: Vec<lsp_types::LocationLink>) -> Self { pub(crate) fn update(
Self { &mut self,
symbol_range, symbol_range: Range<usize>,
locations: Rc::new(locations), locations: Vec<lsp_types::LocationLink>,
) {
self.clear();
self.symbol_range = symbol_range;
self.locations = Rc::new(locations);
}
pub(crate) fn is_empty(&self) -> bool {
self.locations.is_empty()
}
pub(crate) fn clear(&mut self) {
if !self.locations.is_empty() {
self.last_location = Some((self.symbol_range.clone(), self.locations.clone()));
} }
self.symbol_range = 0..0;
self.locations = Rc::new(vec![]);
} }
pub(crate) fn is_same(&self, offset: usize) -> bool { pub(crate) fn is_same(&self, offset: usize) -> bool {
@ -47,19 +64,18 @@ impl HoverDefinition {
} }
impl InputState { impl InputState {
pub(super) fn handle_hover_definition( pub(crate) fn handle_hover_definition(
&mut self, &mut self,
offset: usize, offset: usize,
window: &mut Window, window: &mut Window,
cx: &mut Context<InputState>, cx: &mut Context<Self>,
) { ) {
let Some(provider) = self.lsp.definition_provider.clone() else { let Some(provider) = self.lsp.definition_provider.clone() else {
return; return;
}; };
if let Some(hover_definition) = self.hover_definition.as_ref() {
if hover_definition.is_same(offset) { if self.hover_definition.is_same(offset) {
return; return;
}
} }
// Currently not implemented. // Currently not implemented.
@ -71,7 +87,7 @@ impl InputState {
_ = editor.update(cx, |editor, cx| { _ = editor.update(cx, |editor, cx| {
if locations.is_empty() { if locations.is_empty() {
editor.hover_definition = None; editor.hover_definition.clear();
} else { } else {
if let Some(location) = locations.first() { if let Some(location) = locations.first() {
if let Some(range) = location.origin_selection_range { if let Some(range) = location.origin_selection_range {
@ -81,7 +97,9 @@ impl InputState {
} }
} }
editor.hover_definition = Some(HoverDefinition::new(symbol_range, locations)); editor
.hover_definition
.update(symbol_range.clone(), locations.clone());
} }
cx.notify(); cx.notify();
}); });
@ -90,6 +108,24 @@ impl InputState {
}); });
} }
pub(crate) fn on_action_go_to_definition(
&mut self,
_: &GoToDefinition,
_: &mut Window,
cx: &mut Context<Self>,
) {
let offset = self.cursor();
if let Some((symbol_range, locations)) = self.hover_definition.last_location.clone() {
if !(symbol_range.start..=symbol_range.end).contains(&offset) {
return;
}
if let Some(location) = locations.first().cloned() {
self.go_to_definition(&location, cx);
}
}
}
/// Return true if handled. /// Return true if handled.
pub(crate) fn handle_click_hover_definition( pub(crate) fn handle_click_hover_definition(
&mut self, &mut self,
@ -102,17 +138,27 @@ impl InputState {
return false; return false;
} }
let Some(hover_definition) = self.hover_definition.as_ref() else { if self.hover_definition.is_empty() {
return false; return false;
}; };
if !hover_definition.is_same(offset) { if !self.hover_definition.is_same(offset) {
return false; return false;
} }
let Some(location) = hover_definition.locations.first().cloned() else { let Some(location) = self.hover_definition.locations.first().cloned() else {
return false; return false;
}; };
self.go_to_definition(&location, cx);
true
}
pub(crate) fn go_to_definition(
&mut self,
location: &lsp_types::LocationLink,
cx: &mut Context<Self>,
) {
if location if location
.target_uri .target_uri
.scheme() .scheme()
@ -129,8 +175,6 @@ impl InputState {
self.move_to(start, cx); self.move_to(start, cx);
self.select_to(end, cx); self.select_to(end, cx);
} }
true
} }
} }
@ -144,7 +188,7 @@ impl TextElement {
return None; return None;
} }
let Some(hover_definition) = editor.hover_definition.as_ref() else { if editor.hover_definition.is_empty() {
return None; return None;
}; };
@ -160,7 +204,10 @@ impl TextElement {
..UnderlineStyle::default() ..UnderlineStyle::default()
}); });
Some((hover_definition.symbol_range.clone(), highlight_style)) Some((
editor.hover_definition.symbol_range.clone(),
highlight_style,
))
} }
pub(crate) fn layout_hover_definition_hitbox( pub(crate) fn layout_hover_definition_hitbox(
@ -173,11 +220,11 @@ impl TextElement {
return None; return None;
} }
let Some(hover_definition) = editor.hover_definition.as_ref() else { if editor.hover_definition.is_empty() {
return None; return None;
}; };
let Some(bounds) = editor.range_to_bounds(&hover_definition.symbol_range) else { let Some(bounds) = editor.range_to_bounds(&editor.hover_definition.symbol_range) else {
return None; return None;
}; };

View file

@ -82,6 +82,7 @@ impl InputState {
handled = menu.handle_action(action, window, cx) handled = menu.handle_action(action, window, cx)
}); });
} }
ContextMenu::MouseContext(..) => {}
}; };
handled handled
@ -114,7 +115,7 @@ impl InputState {
self.hover_popover = None; self.hover_popover = None;
self.handle_hover_definition(offset, window, cx); self.handle_hover_definition(offset, window, cx);
} else { } else {
self.hover_definition = None; self.hover_definition.clear();
self.handle_hover_popover(offset, window, cx); self.handle_hover_popover(offset, window, cx);
} }
} }

View file

@ -8,7 +8,7 @@ mod mask_pattern;
mod mode; mod mode;
mod number_input; mod number_input;
mod otp_input; mod otp_input;
mod popovers; pub(crate) mod popovers;
mod rope_ext; mod rope_ext;
mod search; mod search;
mod state; mod state;

View file

@ -0,0 +1,143 @@
use gpui::{
anchored, deferred, div, prelude::FluentBuilder as _, px, App, AppContext as _, Context,
DismissEvent, Entity, IntoElement, MouseDownEvent, ParentElement as _, Pixels, Point, Render,
Styled, Subscription, Window,
};
use rust_i18n::t;
use crate::{
input::{self, popovers::ContextMenu, InputState},
popup_menu::PopupMenu,
};
/// Context menu for mouse right clicks.
pub(crate) struct MouseContextMenu {
editor: Entity<InputState>,
menu: Entity<PopupMenu>,
mouse_position: Point<Pixels>,
open: bool,
_subscriptions: Vec<Subscription>,
}
impl InputState {
pub(crate) fn handle_right_click_menu(
&mut self,
event: &MouseDownEvent,
offset: usize,
window: &mut Window,
cx: &mut Context<Self>,
) {
// Show Mouse context menu
if !self.selected_range.contains(offset) {
self.move_to(offset, cx);
}
self.context_menu = Some(ContextMenu::MouseContext(self.mouse_context_menu.clone()));
let is_code_editor = self.mode.is_code_editor();
if is_code_editor {
self.handle_hover_definition(offset, window, cx);
}
let has_goto_definition = self.lsp.definition_provider.is_some();
let has_code_action = !self.lsp.code_action_providers.is_empty();
let is_selected = !self.selected_range.is_empty();
let has_paste = cx.read_from_clipboard().is_some();
self.mouse_context_menu.update(cx, |this, cx| {
this.mouse_position = event.position;
this.menu.update(cx, |menu, cx| {
let new_menu = 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_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;
cx.notify();
});
this.open = true;
cx.notify();
});
}
}
impl MouseContextMenu {
pub(crate) fn new(
editor: Entity<InputState>,
window: &mut Window,
cx: &mut App,
) -> Entity<Self> {
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>) {
self.open = false;
self.editor.update(cx, |this, cx| {
this.focus(window, cx);
});
}
}
impl Render for MouseContextMenu {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
if !self.open {
return div().into_any_element();
}
let pos = self.mouse_position;
deferred(
anchored()
.snap_to_window_with_margin(px(8.))
.anchor(gpui::Corner::TopLeft)
.position(pos)
.child(
div()
.font_family(".SystemUIFont")
.text_size(px(14.))
.cursor_default()
.child(self.menu.clone()),
),
)
.into_any_element()
}
}

View file

@ -1,10 +1,12 @@
mod code_action_menu; mod code_action_menu;
mod completion_menu; mod completion_menu;
mod context_menu;
mod diagnostic_popover; mod diagnostic_popover;
mod hover_popover; mod hover_popover;
pub(crate) use code_action_menu::*; pub(crate) use code_action_menu::*;
pub(crate) use completion_menu::*; pub(crate) use completion_menu::*;
pub(crate) use context_menu::*;
pub(crate) use diagnostic_popover::*; pub(crate) use diagnostic_popover::*;
pub(crate) use hover_popover::*; pub(crate) use hover_popover::*;
@ -21,6 +23,7 @@ use crate::{
pub(crate) enum ContextMenu { pub(crate) enum ContextMenu {
Completion(Entity<CompletionMenu>), Completion(Entity<CompletionMenu>),
CodeAction(Entity<CodeActionMenu>), CodeAction(Entity<CodeActionMenu>),
MouseContext(Entity<MouseContextMenu>),
} }
impl ContextMenu { impl ContextMenu {
@ -28,6 +31,7 @@ impl ContextMenu {
match self { match self {
ContextMenu::Completion(menu) => menu.read(cx).is_open(), ContextMenu::Completion(menu) => menu.read(cx).is_open(),
ContextMenu::CodeAction(menu) => menu.read(cx).is_open(), ContextMenu::CodeAction(menu) => menu.read(cx).is_open(),
ContextMenu::MouseContext(menu) => menu.read(cx).is_open(),
} }
} }
@ -35,6 +39,7 @@ impl ContextMenu {
match self { match self {
ContextMenu::Completion(menu) => menu.clone().into_any_element(), ContextMenu::Completion(menu) => menu.clone().into_any_element(),
ContextMenu::CodeAction(menu) => menu.clone().into_any_element(), ContextMenu::CodeAction(menu) => menu.clone().into_any_element(),
ContextMenu::MouseContext(menu) => menu.clone().into_any_element(),
} }
} }
} }

View file

@ -30,7 +30,7 @@ use super::{
text_wrapper::TextWrapper, text_wrapper::TextWrapper,
}; };
use crate::input::{ use crate::input::{
popovers::{ContextMenu, DiagnosticPopover, HoverPopover}, popovers::{ContextMenu, DiagnosticPopover, HoverPopover, MouseContextMenu},
search::{self, SearchPanel}, search::{self, SearchPanel},
HoverDefinition, Lsp, Position, HoverDefinition, Lsp, Position,
}; };
@ -92,6 +92,7 @@ actions!(
Escape, Escape,
ToggleCodeActions, ToggleCodeActions,
Search, Search,
GoToDefinition,
] ]
); );
@ -298,11 +299,12 @@ pub struct InputState {
diagnostic_popover: Option<Entity<DiagnosticPopover>>, diagnostic_popover: Option<Entity<DiagnosticPopover>>,
/// Completion/CodeAction context menu /// Completion/CodeAction context menu
pub(super) context_menu: Option<ContextMenu>, pub(super) context_menu: Option<ContextMenu>,
pub(super) mouse_context_menu: Entity<MouseContextMenu>,
/// A flag to indicate if we are currently inserting a completion item. /// A flag to indicate if we are currently inserting a completion item.
pub(super) completion_inserting: bool, pub(super) completion_inserting: bool,
pub(super) hover_popover: Option<Entity<HoverPopover>>, pub(super) hover_popover: Option<Entity<HoverPopover>>,
/// The LSP definitions locations for "Go to Definition" feature. /// The LSP definitions locations for "Go to Definition" feature.
pub(super) hover_definition: Option<HoverDefinition>, pub(super) hover_definition: HoverDefinition,
pub lsp: Lsp, pub lsp: Lsp,
@ -349,6 +351,7 @@ impl InputState {
]; ];
let text_style = window.text_style(); let text_style = window.text_style();
let mouse_context_menu = MouseContextMenu::new(cx.entity(), window, cx);
Self { Self {
focus_handle: focus_handle.clone(), focus_handle: focus_handle.clone(),
@ -390,9 +393,10 @@ impl InputState {
lsp: Lsp::default(), lsp: Lsp::default(),
diagnostic_popover: None, diagnostic_popover: None,
context_menu: None, context_menu: None,
mouse_context_menu,
completion_inserting: false, completion_inserting: false,
hover_popover: None, hover_popover: None,
hover_definition: None, hover_definition: HoverDefinition::default(),
silent_replace_text: false, silent_replace_text: false,
_subscriptions, _subscriptions,
_context_menu_task: Task::ready(Ok(())), _context_menu_task: Task::ready(Ok(())),
@ -1530,15 +1534,6 @@ impl InputState {
cx.propagate(); cx.propagate();
} }
pub(super) fn toggle_code_actions(
&mut self,
_: &ToggleCodeActions,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.handle_code_action_trigger(window, cx)
}
pub(super) fn on_mouse_down( pub(super) fn on_mouse_down(
&mut self, &mut self,
event: &MouseDownEvent, event: &MouseDownEvent,
@ -1566,6 +1561,12 @@ impl InputState {
return; return;
} }
// Show Mouse context menu
if event.button == MouseButton::Right {
self.handle_right_click_menu(event, offset, window, cx);
return;
}
if event.modifiers.shift { if event.modifiers.shift {
self.select_to(offset, cx); self.select_to(offset, cx);
} else { } else {

View file

@ -278,7 +278,9 @@ impl RenderOnce for TextInput {
.on_action(window.listener_for(&self.state, InputState::indent_block)) .on_action(window.listener_for(&self.state, InputState::indent_block))
.on_action(window.listener_for(&self.state, InputState::outdent_block)) .on_action(window.listener_for(&self.state, InputState::outdent_block))
}) })
.on_action(window.listener_for(&self.state, InputState::toggle_code_actions)) .on_action(
window.listener_for(&self.state, InputState::on_action_toggle_code_actions),
)
}) })
.on_action(window.listener_for(&self.state, InputState::left)) .on_action(window.listener_for(&self.state, InputState::left))
.on_action(window.listener_for(&self.state, InputState::right)) .on_action(window.listener_for(&self.state, InputState::right))
@ -291,6 +293,9 @@ impl RenderOnce for TextInput {
.on_action(window.listener_for(&self.state, InputState::select_down)) .on_action(window.listener_for(&self.state, InputState::select_down))
.on_action(window.listener_for(&self.state, InputState::page_up)) .on_action(window.listener_for(&self.state, InputState::page_up))
.on_action(window.listener_for(&self.state, InputState::page_down)) .on_action(window.listener_for(&self.state, InputState::page_down))
.on_action(
window.listener_for(&self.state, InputState::on_action_go_to_definition),
)
}) })
.on_action(window.listener_for(&self.state, InputState::select_all)) .on_action(window.listener_for(&self.state, InputState::select_all))
.on_action(window.listener_for(&self.state, InputState::select_to_start_of_line)) .on_action(window.listener_for(&self.state, InputState::select_to_start_of_line))
@ -313,10 +318,18 @@ impl RenderOnce for TextInput {
MouseButton::Left, MouseButton::Left,
window.listener_for(&self.state, InputState::on_mouse_down), window.listener_for(&self.state, InputState::on_mouse_down),
) )
.on_mouse_down(
MouseButton::Right,
window.listener_for(&self.state, InputState::on_mouse_down),
)
.on_mouse_up( .on_mouse_up(
MouseButton::Left, MouseButton::Left,
window.listener_for(&self.state, InputState::on_mouse_up), window.listener_for(&self.state, InputState::on_mouse_up),
) )
.on_mouse_up(
MouseButton::Right,
window.listener_for(&self.state, InputState::on_mouse_up),
)
.on_mouse_move(window.listener_for(&self.state, InputState::on_mouse_move)) .on_mouse_move(window.listener_for(&self.state, InputState::on_mouse_move))
.on_scroll_wheel(window.listener_for(&self.state, InputState::on_scroll_wheel)) .on_scroll_wheel(window.listener_for(&self.state, InputState::on_scroll_wheel))
.size_full() .size_full()

View file

@ -2,7 +2,7 @@ use std::{cell::RefCell, rc::Rc};
use gpui::{ use gpui::{
anchored, deferred, div, prelude::FluentBuilder, px, relative, AnyElement, App, Context, anchored, deferred, div, prelude::FluentBuilder, px, relative, AnyElement, App, Context,
Corner, DismissEvent, DispatchPhase, Element, ElementId, Entity, Focusable, GlobalElementId, Corner, DismissEvent, Element, ElementId, Entity, Focusable, GlobalElementId,
InspectorElementId, InteractiveElement, IntoElement, MouseButton, MouseDownEvent, InspectorElementId, InteractiveElement, IntoElement, MouseButton, MouseDownEvent,
ParentElement, Pixels, Point, Position, Stateful, Style, Window, ParentElement, Pixels, Point, Position, Stateful, Style, Window,
}; };
@ -224,7 +224,7 @@ impl Element for ContextMenu {
// When right mouse click, to build content menu, and show it at the mouse position. // When right mouse click, to build content menu, and show it at the mouse position.
window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| { window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
if phase == DispatchPhase::Bubble if phase.bubble()
&& event.button == MouseButton::Right && event.button == MouseButton::Right
&& bounds.contains(&event.position) && bounds.contains(&event.position)
{ {

View file

@ -6,18 +6,17 @@ use crate::{
button::Button, h_flex, popover::Popover, v_flex, ActiveTheme, Icon, IconName, Selectable, button::Button, h_flex, popover::Popover, v_flex, ActiveTheme, Icon, IconName, Selectable,
Sizable as _, Sizable as _,
}; };
use crate::{Kbd, Side, StyledExt}; use crate::{Kbd, Side, Size, StyledExt};
use gpui::{ use gpui::{
anchored, canvas, div, prelude::FluentBuilder, px, rems, Action, AnyElement, App, AppContext, anchored, canvas, div, prelude::FluentBuilder, px, rems, Action, AnyElement, App, AppContext,
Bounds, Context, Corner, DismissEvent, Edges, Entity, EventEmitter, FocusHandle, Focusable, Bounds, Context, Corner, DismissEvent, Edges, Entity, EventEmitter, FocusHandle, Focusable,
InteractiveElement, IntoElement, KeyBinding, ParentElement, Pixels, Render, ScrollHandle, InteractiveElement, IntoElement, KeyBinding, ParentElement, Pixels, Render, ScrollHandle,
SharedString, StatefulInteractiveElement, Styled, WeakEntity, Window, SharedString, StatefulInteractiveElement, Styled, WeakEntity, Window,
}; };
use gpui::{AsKeystroke, MouseDownEvent, Subscription}; use gpui::{AsKeystroke, Half, MouseDownEvent, Subscription};
use std::ops::Deref; use std::ops::Deref;
use std::rc::Rc; use std::rc::Rc;
const ITEM_HEIGHT: Pixels = px(26.);
const CONTEXT: &str = "PopupMenu"; const CONTEXT: &str = "PopupMenu";
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
@ -61,7 +60,7 @@ pub trait PopupMenuExt: Styled + Selectable + InteractiveElement + IntoElement +
} }
impl PopupMenuExt for Button {} impl PopupMenuExt for Button {}
enum PopupMenuItem { pub(crate) enum PopupMenuItem {
Separator, Separator,
Label(SharedString), Label(SharedString),
Item { Item {
@ -113,13 +112,14 @@ pub struct PopupMenu {
/// The parent menu of this menu, if this is a submenu /// The parent menu of this menu, if this is a submenu
parent_menu: Option<WeakEntity<Self>>, parent_menu: Option<WeakEntity<Self>>,
focus_handle: FocusHandle, focus_handle: FocusHandle,
menu_items: Vec<PopupMenuItem>, pub(crate) menu_items: Vec<PopupMenuItem>,
has_icon: bool, has_icon: bool,
selected_index: Option<usize>, selected_index: Option<usize>,
min_width: Option<Pixels>, min_width: Option<Pixels>,
max_width: Option<Pixels>, max_width: Option<Pixels>,
max_height: Option<Pixels>, max_height: Option<Pixels>,
bounds: Bounds<Pixels>, bounds: Bounds<Pixels>,
size: Size,
scrollable: bool, scrollable: bool,
external_link_icon: bool, external_link_icon: bool,
@ -131,32 +131,35 @@ pub struct PopupMenu {
} }
impl PopupMenu { impl PopupMenu {
pub(crate) fn new(cx: &mut App) -> Self {
Self {
focus_handle: cx.focus_handle(),
previous_focus_handle: None,
parent_menu: None,
menu_items: Vec::new(),
selected_index: None,
min_width: None,
max_width: None,
max_height: None,
has_icon: false,
bounds: Bounds::default(),
scrollable: false,
scroll_handle: ScrollHandle::default(),
scroll_state: ScrollbarState::default(),
external_link_icon: true,
size: Size::default(),
_subscriptions: vec![],
}
}
pub fn build( pub fn build(
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
f: impl FnOnce(Self, &mut Window, &mut Context<PopupMenu>) -> Self, f: impl FnOnce(Self, &mut Window, &mut Context<PopupMenu>) -> Self,
) -> Entity<Self> { ) -> Entity<Self> {
cx.new(|cx| { cx.new(|cx| {
let focus_handle = cx.focus_handle(); let mut menu = Self::new(cx);
let _subscriptions = vec![]; menu.previous_focus_handle = window.focused(cx);
let menu = Self {
focus_handle,
previous_focus_handle: window.focused(cx),
parent_menu: None,
menu_items: Vec::new(),
selected_index: None,
min_width: None,
max_width: None,
max_height: None,
has_icon: false,
bounds: Bounds::default(),
scrollable: false,
scroll_handle: ScrollHandle::default(),
scroll_state: ScrollbarState::default(),
external_link_icon: true,
_subscriptions,
};
f(menu, window, cx) f(menu, window, cx)
}) })
} }
@ -198,6 +201,17 @@ impl PopupMenu {
self.menu_with_disabled(label, action, false) self.menu_with_disabled(label, action, false)
} }
/// Add Menu Item with enable state
pub fn menu_with_enable(
mut self,
label: impl Into<SharedString>,
action: Box<dyn Action>,
enable: bool,
) -> Self {
self.add_menu_item(label, None, action, !enable);
self
}
/// Add Menu Item with disabled state /// Add Menu Item with disabled state
pub fn menu_with_disabled( pub fn menu_with_disabled(
mut self, mut self,
@ -428,6 +442,12 @@ impl PopupMenu {
}) })
} }
/// Use small size, the menu item will have smaller height.
pub(crate) fn small(mut self) -> Self {
self.size = Size::Small;
self
}
/// Add a separator Menu Item /// Add a separator Menu Item
pub fn separator(mut self) -> Self { pub fn separator(mut self) -> Self {
if self.menu_items.is_empty() { if self.menu_items.is_empty() {
@ -805,12 +825,17 @@ impl PopupMenu {
const EDGE_PADDING: Pixels = px(8.); const EDGE_PADDING: Pixels = px(8.);
const INNER_PADDING: Pixels = px(4.); const INNER_PADDING: Pixels = px(4.);
let (item_height, radius) = match self.size {
Size::Small => (px(20.), state.radius.half()),
_ => (px(26.), state.radius),
};
let this = MenuItem::new(ix) let this = MenuItem::new(ix)
.relative() .relative()
.text_sm() .text_sm()
.py_0() .py_0()
.px(INNER_PADDING) .px(INNER_PADDING)
.rounded(state.radius) .rounded(radius)
.items_center() .items_center()
.hovered(selected) .hovered(selected)
.on_mouse_enter(cx.listener(move |this, _, _, cx| { .on_mouse_enter(cx.listener(move |this, _, _, cx| {
@ -853,7 +878,7 @@ impl PopupMenu {
.disabled(*disabled) .disabled(*disabled)
.child( .child(
h_flex() h_flex()
.min_h(ITEM_HEIGHT) .min_h(item_height)
.items_center() .items_center()
.gap_x_1() .gap_x_1()
.children(Self::render_icon(has_icon, icon.clone(), window, cx)) .children(Self::render_icon(has_icon, icon.clone(), window, cx))
@ -877,7 +902,7 @@ impl PopupMenu {
) )
}) })
.disabled(*disabled) .disabled(*disabled)
.h(ITEM_HEIGHT) .h(item_height)
.children(Self::render_icon(has_icon, icon.clone(), window, cx)) .children(Self::render_icon(has_icon, icon.clone(), window, cx))
.child( .child(
h_flex() h_flex()
@ -914,7 +939,7 @@ impl PopupMenu {
.items_start() .items_start()
.child( .child(
h_flex() h_flex()
.min_h(ITEM_HEIGHT) .min_h(item_height)
.size_full() .size_full()
.items_center() .items_center()
.gap_x_1() .gap_x_1()