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:
parent
993ce34bd8
commit
bf5650b5db
12 changed files with 342 additions and 71 deletions
|
|
@ -160,9 +160,31 @@ Input:
|
|||
en: Replace
|
||||
zh-CN: 替换
|
||||
zh-HK: 替換
|
||||
it: Sostituisci
|
||||
Replace All:
|
||||
en: Replace All
|
||||
zh-CN: 全部替换
|
||||
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: 顯示代碼操作
|
||||
|
|
|
|||
|
|
@ -25,6 +25,11 @@ impl Selection {
|
|||
self.start = 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 {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use std::ops::Range;
|
|||
|
||||
use crate::input::{
|
||||
popovers::{CodeActionItem, CodeActionMenu, ContextMenu},
|
||||
InputState,
|
||||
InputState, ToggleCodeActions,
|
||||
};
|
||||
|
||||
pub trait CodeActionProvider {
|
||||
|
|
@ -37,6 +37,15 @@ pub trait CodeActionProvider {
|
|||
}
|
||||
|
||||
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.
|
||||
pub(crate) fn handle_code_action_trigger(
|
||||
&mut self,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use rope::Rope;
|
|||
use std::{ops::Range, rc::Rc};
|
||||
|
||||
use crate::{
|
||||
input::{element::TextElement, InputState, RopeExt},
|
||||
input::{element::TextElement, GoToDefinition, InputState, RopeExt},
|
||||
ActiveTheme,
|
||||
};
|
||||
|
||||
|
|
@ -31,14 +31,31 @@ pub(crate) struct HoverDefinition {
|
|||
/// The range of the symbol that triggered the hover.
|
||||
symbol_range: Range<usize>,
|
||||
pub(crate) locations: Rc<Vec<lsp_types::LocationLink>>,
|
||||
last_location: Option<(Range<usize>, Rc<Vec<lsp_types::LocationLink>>)>,
|
||||
}
|
||||
|
||||
impl HoverDefinition {
|
||||
pub(crate) fn new(symbol_range: Range<usize>, locations: Vec<lsp_types::LocationLink>) -> Self {
|
||||
Self {
|
||||
symbol_range,
|
||||
locations: Rc::new(locations),
|
||||
pub(crate) fn update(
|
||||
&mut self,
|
||||
symbol_range: Range<usize>,
|
||||
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 {
|
||||
|
|
@ -47,19 +64,18 @@ impl HoverDefinition {
|
|||
}
|
||||
|
||||
impl InputState {
|
||||
pub(super) fn handle_hover_definition(
|
||||
pub(crate) fn handle_hover_definition(
|
||||
&mut self,
|
||||
offset: usize,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<InputState>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(provider) = self.lsp.definition_provider.clone() else {
|
||||
return;
|
||||
};
|
||||
if let Some(hover_definition) = self.hover_definition.as_ref() {
|
||||
if hover_definition.is_same(offset) {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.hover_definition.is_same(offset) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Currently not implemented.
|
||||
|
|
@ -71,7 +87,7 @@ impl InputState {
|
|||
|
||||
_ = editor.update(cx, |editor, cx| {
|
||||
if locations.is_empty() {
|
||||
editor.hover_definition = None;
|
||||
editor.hover_definition.clear();
|
||||
} else {
|
||||
if let Some(location) = locations.first() {
|
||||
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();
|
||||
});
|
||||
|
|
@ -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.
|
||||
pub(crate) fn handle_click_hover_definition(
|
||||
&mut self,
|
||||
|
|
@ -102,17 +138,27 @@ impl InputState {
|
|||
return false;
|
||||
}
|
||||
|
||||
let Some(hover_definition) = self.hover_definition.as_ref() else {
|
||||
if self.hover_definition.is_empty() {
|
||||
return false;
|
||||
};
|
||||
if !hover_definition.is_same(offset) {
|
||||
if !self.hover_definition.is_same(offset) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some(location) = hover_definition.locations.first().cloned() else {
|
||||
let Some(location) = self.hover_definition.locations.first().cloned() else {
|
||||
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
|
||||
.target_uri
|
||||
.scheme()
|
||||
|
|
@ -129,8 +175,6 @@ impl InputState {
|
|||
self.move_to(start, cx);
|
||||
self.select_to(end, cx);
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -144,7 +188,7 @@ impl TextElement {
|
|||
return None;
|
||||
}
|
||||
|
||||
let Some(hover_definition) = editor.hover_definition.as_ref() else {
|
||||
if editor.hover_definition.is_empty() {
|
||||
return None;
|
||||
};
|
||||
|
||||
|
|
@ -160,7 +204,10 @@ impl TextElement {
|
|||
..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(
|
||||
|
|
@ -173,11 +220,11 @@ impl TextElement {
|
|||
return None;
|
||||
}
|
||||
|
||||
let Some(hover_definition) = editor.hover_definition.as_ref() else {
|
||||
if editor.hover_definition.is_empty() {
|
||||
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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ impl InputState {
|
|||
handled = menu.handle_action(action, window, cx)
|
||||
});
|
||||
}
|
||||
ContextMenu::MouseContext(..) => {}
|
||||
};
|
||||
|
||||
handled
|
||||
|
|
@ -114,7 +115,7 @@ impl InputState {
|
|||
self.hover_popover = None;
|
||||
self.handle_hover_definition(offset, window, cx);
|
||||
} else {
|
||||
self.hover_definition = None;
|
||||
self.hover_definition.clear();
|
||||
self.handle_hover_popover(offset, window, cx);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ mod mask_pattern;
|
|||
mod mode;
|
||||
mod number_input;
|
||||
mod otp_input;
|
||||
mod popovers;
|
||||
pub(crate) mod popovers;
|
||||
mod rope_ext;
|
||||
mod search;
|
||||
mod state;
|
||||
|
|
|
|||
143
crates/ui/src/input/popovers/context_menu.rs
Normal file
143
crates/ui/src/input/popovers/context_menu.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
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::*;
|
||||
|
||||
|
|
@ -21,6 +23,7 @@ use crate::{
|
|||
pub(crate) enum ContextMenu {
|
||||
Completion(Entity<CompletionMenu>),
|
||||
CodeAction(Entity<CodeActionMenu>),
|
||||
MouseContext(Entity<MouseContextMenu>),
|
||||
}
|
||||
|
||||
impl ContextMenu {
|
||||
|
|
@ -28,6 +31,7 @@ impl ContextMenu {
|
|||
match self {
|
||||
ContextMenu::Completion(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 {
|
||||
ContextMenu::Completion(menu) => menu.clone().into_any_element(),
|
||||
ContextMenu::CodeAction(menu) => menu.clone().into_any_element(),
|
||||
ContextMenu::MouseContext(menu) => menu.clone().into_any_element(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ use super::{
|
|||
text_wrapper::TextWrapper,
|
||||
};
|
||||
use crate::input::{
|
||||
popovers::{ContextMenu, DiagnosticPopover, HoverPopover},
|
||||
popovers::{ContextMenu, DiagnosticPopover, HoverPopover, MouseContextMenu},
|
||||
search::{self, SearchPanel},
|
||||
HoverDefinition, Lsp, Position,
|
||||
};
|
||||
|
|
@ -92,6 +92,7 @@ actions!(
|
|||
Escape,
|
||||
ToggleCodeActions,
|
||||
Search,
|
||||
GoToDefinition,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
@ -298,11 +299,12 @@ pub struct InputState {
|
|||
diagnostic_popover: Option<Entity<DiagnosticPopover>>,
|
||||
/// Completion/CodeAction context menu
|
||||
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.
|
||||
pub(super) completion_inserting: bool,
|
||||
pub(super) hover_popover: Option<Entity<HoverPopover>>,
|
||||
/// The LSP definitions locations for "Go to Definition" feature.
|
||||
pub(super) hover_definition: Option<HoverDefinition>,
|
||||
pub(super) hover_definition: HoverDefinition,
|
||||
|
||||
pub lsp: Lsp,
|
||||
|
||||
|
|
@ -349,6 +351,7 @@ impl InputState {
|
|||
];
|
||||
|
||||
let text_style = window.text_style();
|
||||
let mouse_context_menu = MouseContextMenu::new(cx.entity(), window, cx);
|
||||
|
||||
Self {
|
||||
focus_handle: focus_handle.clone(),
|
||||
|
|
@ -390,9 +393,10 @@ impl InputState {
|
|||
lsp: Lsp::default(),
|
||||
diagnostic_popover: None,
|
||||
context_menu: None,
|
||||
mouse_context_menu,
|
||||
completion_inserting: false,
|
||||
hover_popover: None,
|
||||
hover_definition: None,
|
||||
hover_definition: HoverDefinition::default(),
|
||||
silent_replace_text: false,
|
||||
_subscriptions,
|
||||
_context_menu_task: Task::ready(Ok(())),
|
||||
|
|
@ -1530,15 +1534,6 @@ impl InputState {
|
|||
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(
|
||||
&mut self,
|
||||
event: &MouseDownEvent,
|
||||
|
|
@ -1566,6 +1561,12 @@ impl InputState {
|
|||
return;
|
||||
}
|
||||
|
||||
// Show Mouse context menu
|
||||
if event.button == MouseButton::Right {
|
||||
self.handle_right_click_menu(event, offset, window, cx);
|
||||
return;
|
||||
}
|
||||
|
||||
if event.modifiers.shift {
|
||||
self.select_to(offset, cx);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -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::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::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::page_up))
|
||||
.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_to_start_of_line))
|
||||
|
|
@ -313,10 +318,18 @@ impl RenderOnce for TextInput {
|
|||
MouseButton::Left,
|
||||
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(
|
||||
MouseButton::Left,
|
||||
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_scroll_wheel(window.listener_for(&self.state, InputState::on_scroll_wheel))
|
||||
.size_full()
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::{cell::RefCell, rc::Rc};
|
|||
|
||||
use gpui::{
|
||||
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,
|
||||
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.
|
||||
window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
|
||||
if phase == DispatchPhase::Bubble
|
||||
if phase.bubble()
|
||||
&& event.button == MouseButton::Right
|
||||
&& bounds.contains(&event.position)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,18 +6,17 @@ use crate::{
|
|||
button::Button, h_flex, popover::Popover, v_flex, ActiveTheme, Icon, IconName, Selectable,
|
||||
Sizable as _,
|
||||
};
|
||||
use crate::{Kbd, Side, StyledExt};
|
||||
use crate::{Kbd, Side, Size, StyledExt};
|
||||
use gpui::{
|
||||
anchored, canvas, div, prelude::FluentBuilder, px, rems, Action, AnyElement, App, AppContext,
|
||||
Bounds, Context, Corner, DismissEvent, Edges, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
InteractiveElement, IntoElement, KeyBinding, ParentElement, Pixels, Render, ScrollHandle,
|
||||
SharedString, StatefulInteractiveElement, Styled, WeakEntity, Window,
|
||||
};
|
||||
use gpui::{AsKeystroke, MouseDownEvent, Subscription};
|
||||
use gpui::{AsKeystroke, Half, MouseDownEvent, Subscription};
|
||||
use std::ops::Deref;
|
||||
use std::rc::Rc;
|
||||
|
||||
const ITEM_HEIGHT: Pixels = px(26.);
|
||||
const CONTEXT: &str = "PopupMenu";
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
|
|
@ -61,7 +60,7 @@ pub trait PopupMenuExt: Styled + Selectable + InteractiveElement + IntoElement +
|
|||
}
|
||||
impl PopupMenuExt for Button {}
|
||||
|
||||
enum PopupMenuItem {
|
||||
pub(crate) enum PopupMenuItem {
|
||||
Separator,
|
||||
Label(SharedString),
|
||||
Item {
|
||||
|
|
@ -113,13 +112,14 @@ pub struct PopupMenu {
|
|||
/// The parent menu of this menu, if this is a submenu
|
||||
parent_menu: Option<WeakEntity<Self>>,
|
||||
focus_handle: FocusHandle,
|
||||
menu_items: Vec<PopupMenuItem>,
|
||||
pub(crate) menu_items: Vec<PopupMenuItem>,
|
||||
has_icon: bool,
|
||||
selected_index: Option<usize>,
|
||||
min_width: Option<Pixels>,
|
||||
max_width: Option<Pixels>,
|
||||
max_height: Option<Pixels>,
|
||||
bounds: Bounds<Pixels>,
|
||||
size: Size,
|
||||
|
||||
scrollable: bool,
|
||||
external_link_icon: bool,
|
||||
|
|
@ -131,32 +131,35 @@ pub struct 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(
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
f: impl FnOnce(Self, &mut Window, &mut Context<PopupMenu>) -> Self,
|
||||
) -> Entity<Self> {
|
||||
cx.new(|cx| {
|
||||
let focus_handle = cx.focus_handle();
|
||||
let _subscriptions = vec![];
|
||||
|
||||
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,
|
||||
};
|
||||
let mut menu = Self::new(cx);
|
||||
menu.previous_focus_handle = window.focused(cx);
|
||||
f(menu, window, cx)
|
||||
})
|
||||
}
|
||||
|
|
@ -198,6 +201,17 @@ impl PopupMenu {
|
|||
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
|
||||
pub fn menu_with_disabled(
|
||||
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
|
||||
pub fn separator(mut self) -> Self {
|
||||
if self.menu_items.is_empty() {
|
||||
|
|
@ -805,12 +825,17 @@ impl PopupMenu {
|
|||
const EDGE_PADDING: Pixels = px(8.);
|
||||
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)
|
||||
.relative()
|
||||
.text_sm()
|
||||
.py_0()
|
||||
.px(INNER_PADDING)
|
||||
.rounded(state.radius)
|
||||
.rounded(radius)
|
||||
.items_center()
|
||||
.hovered(selected)
|
||||
.on_mouse_enter(cx.listener(move |this, _, _, cx| {
|
||||
|
|
@ -853,7 +878,7 @@ impl PopupMenu {
|
|||
.disabled(*disabled)
|
||||
.child(
|
||||
h_flex()
|
||||
.min_h(ITEM_HEIGHT)
|
||||
.min_h(item_height)
|
||||
.items_center()
|
||||
.gap_x_1()
|
||||
.children(Self::render_icon(has_icon, icon.clone(), window, cx))
|
||||
|
|
@ -877,7 +902,7 @@ impl PopupMenu {
|
|||
)
|
||||
})
|
||||
.disabled(*disabled)
|
||||
.h(ITEM_HEIGHT)
|
||||
.h(item_height)
|
||||
.children(Self::render_icon(has_icon, icon.clone(), window, cx))
|
||||
.child(
|
||||
h_flex()
|
||||
|
|
@ -914,7 +939,7 @@ impl PopupMenu {
|
|||
.items_start()
|
||||
.child(
|
||||
h_flex()
|
||||
.min_h(ITEM_HEIGHT)
|
||||
.min_h(item_height)
|
||||
.size_full()
|
||||
.items_center()
|
||||
.gap_x_1()
|
||||
|
|
|
|||
Loading…
Reference in a new issue