Skip to content

Commit fd0b64e

Browse files
committed
fix: handle context menu on mouse release
1 parent 3a146af commit fd0b64e

9 files changed

Lines changed: 222 additions & 55 deletions

File tree

‎crates/db_view/src/db_tree_view.rs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2744,9 +2744,11 @@ impl DbTreeView {
27442744
let view_for_click = view.clone();
27452745
let view_for_double_click = view.clone();
27462746
let view_for_context = view.clone();
2747+
let view_for_context_up = view.clone();
27472748
let node_id_for_click = node_id.clone();
27482749
let node_id_for_double_click = node_id.clone();
27492750
let node_id_for_context = node_id.clone();
2751+
let node_id_for_context_up = node_id.clone();
27502752
let view_for_filter = view.clone();
27512753
let node_id_for_filter = node_id.clone();
27522754

@@ -2800,6 +2802,12 @@ impl DbTreeView {
28002802
cx.notify();
28012803
});
28022804
})
2805+
.on_mouse_up(MouseButton::Right, move |_event, _window, cx| {
2806+
view_for_context_up.update(cx, |this, cx| {
2807+
this.set_context_menu_target(&node_id_for_context_up, ix);
2808+
cx.notify();
2809+
});
2810+
})
28032811
// 内容
28042812
.child(
28052813
h_flex()

‎crates/mongodb_view/src/mongo_tree_view.rs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1252,7 +1252,9 @@ impl MongoTreeView {
12521252
let view_for_double_click = cx.entity().clone();
12531253
let node_id_for_double_click = node_id.clone();
12541254
let view_for_context = cx.entity().clone();
1255+
let view_for_context_up = cx.entity().clone();
12551256
let node_id_for_context = node_id.clone();
1257+
let node_id_for_context_up = node_id.clone();
12561258
let view_for_arrow = cx.entity().clone();
12571259
let node_id_for_arrow = node_id.clone();
12581260

@@ -1284,6 +1286,12 @@ impl MongoTreeView {
12841286
cx.notify();
12851287
});
12861288
})
1289+
.on_mouse_up(MouseButton::Right, move |_event, _window, cx| {
1290+
view_for_context_up.update(cx, |view, cx| {
1291+
view.set_context_menu_target(&node_id_for_context_up);
1292+
cx.notify();
1293+
});
1294+
})
12871295
.child(div().w(indent))
12881296
.child(
12891297
div()

‎crates/one_ui/src/edit_table/state.rs‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1030,6 +1030,10 @@ where
10301030
_: &mut Window,
10311031
_: &mut Context<Self>,
10321032
) {
1033+
self.set_row_context_menu_target(row_ix);
1034+
}
1035+
1036+
fn set_row_context_menu_target(&mut self, row_ix: usize) {
10331037
self.right_clicked_row = Some(row_ix);
10341038
}
10351039

@@ -2822,6 +2826,12 @@ where
28222826
this.on_row_right_click(e, row_ix, window, cx);
28232827
}),
28242828
)
2829+
.on_mouse_up(
2830+
MouseButton::Right,
2831+
cx.listener(move |this, _, _, _| {
2832+
this.set_row_context_menu_target(row_ix);
2833+
}),
2834+
)
28252835
} else {
28262836
self.delegate
28272837
.render_tr(row_ix, window, cx)

‎crates/redis_view/src/redis_tree_view.rs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1975,8 +1975,10 @@ impl RedisTreeView {
19751975
let view = cx.entity().clone();
19761976
let view_for_db_switch = cx.entity().clone();
19771977
let view_for_context = cx.entity().clone();
1978+
let view_for_context_up = cx.entity().clone();
19781979
let view_for_dbl = cx.entity().clone();
19791980
let node_id_for_context = node_id.clone();
1981+
let node_id_for_context_up = node_id.clone();
19801982
let node_id_for_dbl = node_id.clone();
19811983
let connection_id_for_load_more = node_connection_id.clone();
19821984

@@ -2042,6 +2044,12 @@ impl RedisTreeView {
20422044
cx.notify();
20432045
});
20442046
})
2047+
.on_mouse_up(MouseButton::Right, move |_event, _window, cx| {
2048+
view_for_context_up.update(cx, |view, cx| {
2049+
view.set_context_menu_target(&node_id_for_context_up);
2050+
cx.notify();
2051+
});
2052+
})
20452053
// 展开/折叠箭头
20462054
.child(
20472055
div()

‎crates/ui/src/input/popovers/context_menu.rs‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use gpui::{
2-
Anchor, App, AppContext as _, Context, DismissEvent, Entity, IntoElement, MouseDownEvent,
3-
ParentElement as _, Pixels, Point, Render, Styled, Subscription, Window, anchored, deferred,
4-
div, prelude::FluentBuilder as _, px,
2+
Anchor, App, AppContext as _, Context, DismissEvent, Entity, IntoElement, ParentElement as _,
3+
Pixels, Point, Render, Styled, Subscription, Window, anchored, deferred, div,
4+
prelude::FluentBuilder as _, px,
55
};
66
use rust_i18n::t;
77

@@ -98,7 +98,7 @@ impl InputState {
9898

9999
pub(crate) fn handle_right_click_menu(
100100
&mut self,
101-
event: &MouseDownEvent,
101+
position: Point<Pixels>,
102102
offset: usize,
103103
window: &mut Window,
104104
cx: &mut Context<Self>,
@@ -130,7 +130,7 @@ impl InputState {
130130

131131
let action_context = self.focus_handle.clone();
132132
self.context_menu.update(cx, |this, cx| {
133-
this.mouse_position = event.position;
133+
this.mouse_position = position;
134134
this.menu.update(cx, |menu, cx| {
135135
let new_menu = if let Some(builder) = &self.context_menu_builder {
136136
builder(PopupMenu::new(cx), window, cx)

‎crates/ui/src/input/state.rs‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1577,7 +1577,7 @@ impl InputState {
15771577
// Show Mouse context menu
15781578
if event.button == MouseButton::Right {
15791579
if self.enable_context_menu || self.context_menu_builder.is_some() {
1580-
self.handle_right_click_menu(event, offset, window, cx);
1580+
self.handle_right_click_menu(event.position, offset, window, cx);
15811581
}
15821582
return;
15831583
}
@@ -1591,10 +1591,18 @@ impl InputState {
15911591

15921592
pub(super) fn on_mouse_up(
15931593
&mut self,
1594-
_: &MouseUpEvent,
1595-
_window: &mut Window,
1596-
_cx: &mut Context<Self>,
1594+
event: &MouseUpEvent,
1595+
window: &mut Window,
1596+
cx: &mut Context<Self>,
15971597
) {
1598+
if event.button == MouseButton::Right && self.context_menu_content.is_none() {
1599+
if self.enable_context_menu || self.context_menu_builder.is_some() {
1600+
let offset = self.index_for_mouse_position(event.position);
1601+
self.handle_right_click_menu(event.position, offset, window, cx);
1602+
}
1603+
return;
1604+
}
1605+
15981606
if self.selected_range.is_empty() {
15991607
self.selection_reversed = false;
16001608
}

‎crates/ui/src/menu/context_menu.rs‎

Lines changed: 129 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ use std::{cell::RefCell, rc::Rc};
33
use gpui::{
44
Anchor, AnyElement, App, Context, DismissEvent, Element, ElementId, Entity, Focusable,
55
GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId, InteractiveElement, IntoElement,
6-
MouseButton, MouseDownEvent, ParentElement, Pixels, Point, StyleRefinement, Styled,
7-
Subscription, Window, anchored, deferred, div, prelude::FluentBuilder, px,
6+
MouseButton, MouseDownEvent, MouseUpEvent, ParentElement, Pixels, Point, StyleRefinement,
7+
Styled, Subscription, Window, anchored, deferred, div, prelude::FluentBuilder, px,
88
};
99

1010
use crate::menu::PopupMenu;
1111

12+
type PopupMenuBuilder = Rc<dyn Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu>;
13+
1214
/// A extension trait for adding a context menu to an element.
1315
pub trait ContextMenuExt: InteractiveElement + ParentElement + Styled {
1416
/// Add a context menu to the element.
@@ -40,7 +42,7 @@ impl<E: InteractiveElement + ParentElement + Styled> ContextMenuExt for E {}
4042
pub struct ContextMenu<E: ParentElement + Styled + Sized> {
4143
id: ElementId,
4244
element: Option<E>,
43-
menu: Option<Rc<dyn Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu>>,
45+
menu: Option<PopupMenuBuilder>,
4446
// This is not in use, just for style refinement forwarding.
4547
_ignore_style: StyleRefinement,
4648
anchor: Anchor,
@@ -119,6 +121,57 @@ struct ContextMenuSharedState {
119121
_subscription: Option<Subscription>,
120122
}
121123

124+
fn should_open_context_menu(state: &ContextMenuSharedState, position: Point<Pixels>) -> bool {
125+
!state.open || state.position != position
126+
}
127+
128+
fn mark_context_menu_open(state: &mut ContextMenuSharedState, position: Point<Pixels>) {
129+
state.menu_view = None;
130+
state._subscription = None;
131+
state.position = position;
132+
state.open = true;
133+
}
134+
135+
fn open_context_menu_at(
136+
shared_state: Rc<RefCell<ContextMenuSharedState>>,
137+
builder: Option<PopupMenuBuilder>,
138+
position: Point<Pixels>,
139+
window: &mut Window,
140+
cx: &mut App,
141+
) {
142+
{
143+
let mut state = shared_state.borrow_mut();
144+
if !should_open_context_menu(&state, position) {
145+
return;
146+
}
147+
mark_context_menu_open(&mut state, position);
148+
}
149+
150+
window.defer(cx, move |window, cx| {
151+
let menu = PopupMenu::build(window, cx, move |menu, window, cx| {
152+
let Some(build) = &builder else {
153+
return menu;
154+
};
155+
build(menu, window, cx)
156+
});
157+
158+
let _subscription = window.subscribe(&menu, cx, {
159+
let shared_state = shared_state.clone();
160+
move |_, _: &DismissEvent, window, _cx| {
161+
shared_state.borrow_mut().open = false;
162+
window.refresh();
163+
}
164+
});
165+
166+
{
167+
let mut state = shared_state.borrow_mut();
168+
state.menu_view = Some(menu.clone());
169+
state._subscription = Some(_subscription);
170+
window.refresh();
171+
}
172+
});
173+
}
174+
122175
pub struct ContextMenuState {
123176
element: Option<AnyElement>,
124177
shared_state: Rc<RefCell<ContextMenuSharedState>>,
@@ -270,56 +323,86 @@ impl<E: ParentElement + Styled + IntoElement + 'static> Element for ContextMenu<
270323
|_view, state: &mut ContextMenuState, window, _| {
271324
let shared_state = state.shared_state.clone();
272325

273-
let hitbox = hitbox.clone();
274-
// When right mouse click, to build content menu, and show it at the mouse position.
326+
let hitbox_for_down = hitbox.clone();
327+
let state_for_down = shared_state.clone();
328+
let builder_for_down = builder.clone();
329+
// Build the context menu from mouse down on platforms that emit it.
275330
window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
276331
if phase.bubble()
277332
&& event.button == MouseButton::Right
278-
&& hitbox.is_hovered(window)
333+
&& hitbox_for_down.is_hovered(window)
279334
{
280-
{
281-
let mut shared_state = shared_state.borrow_mut();
282-
// Clear any existing menu view to allow immediate replacement
283-
// Set the new position and open the menu
284-
shared_state.menu_view = None;
285-
shared_state._subscription = None;
286-
shared_state.position = event.position;
287-
shared_state.open = true;
288-
}
289-
290-
// Use defer to build the menu in the next frame, avoiding race conditions
291-
window.defer(cx, {
292-
let shared_state = shared_state.clone();
293-
let builder = builder.clone();
294-
move |window, cx| {
295-
let menu = PopupMenu::build(window, cx, move |menu, window, cx| {
296-
let Some(build) = &builder else {
297-
return menu;
298-
};
299-
build(menu, window, cx)
300-
});
301-
302-
// Set up the subscription for dismiss handling
303-
let _subscription = window.subscribe(&menu, cx, {
304-
let shared_state = shared_state.clone();
305-
move |_, _: &DismissEvent, window, _cx| {
306-
shared_state.borrow_mut().open = false;
307-
window.refresh();
308-
}
309-
});
310-
311-
// Update the shared state with the built menu and subscription
312-
{
313-
let mut state = shared_state.borrow_mut();
314-
state.menu_view = Some(menu.clone());
315-
state._subscription = Some(_subscription);
316-
window.refresh();
317-
}
318-
}
319-
});
335+
open_context_menu_at(
336+
state_for_down.clone(),
337+
builder_for_down.clone(),
338+
event.position,
339+
window,
340+
cx,
341+
);
342+
}
343+
});
344+
345+
let hitbox_for_up = hitbox.clone();
346+
let state_for_up = shared_state.clone();
347+
let builder_for_up = builder.clone();
348+
// Some platforms, including the OHOS backend, surface context-menu
349+
// clicks on button release instead of button press.
350+
window.on_mouse_event(move |event: &MouseUpEvent, phase, window, cx| {
351+
if phase.bubble()
352+
&& event.button == MouseButton::Right
353+
&& hitbox_for_up.is_hovered(window)
354+
{
355+
open_context_menu_at(
356+
state_for_up.clone(),
357+
builder_for_up.clone(),
358+
event.position,
359+
window,
360+
cx,
361+
);
320362
}
321363
});
322364
},
323365
);
324366
}
325367
}
368+
369+
#[cfg(test)]
370+
mod tests {
371+
use super::*;
372+
use gpui::px;
373+
374+
fn test_state(position: Point<Pixels>, open: bool) -> ContextMenuSharedState {
375+
ContextMenuSharedState {
376+
menu_view: None,
377+
open,
378+
position,
379+
_subscription: None,
380+
}
381+
}
382+
383+
fn point(x: f32, y: f32) -> Point<Pixels> {
384+
Point { x: px(x), y: px(y) }
385+
}
386+
387+
#[test]
388+
fn context_menu_suppresses_same_click_release_after_press() {
389+
let position = point(10.0, 20.0);
390+
let mut state = test_state(position, false);
391+
392+
assert!(should_open_context_menu(&state, position));
393+
mark_context_menu_open(&mut state, position);
394+
395+
assert!(!should_open_context_menu(&state, position));
396+
}
397+
398+
#[test]
399+
fn context_menu_allows_release_only_or_new_position() {
400+
let position = point(10.0, 20.0);
401+
let new_position = point(20.0, 20.0);
402+
let open_state = test_state(position, true);
403+
let closed_state = test_state(position, false);
404+
405+
assert!(should_open_context_menu(&closed_state, position));
406+
assert!(should_open_context_menu(&open_state, new_position));
407+
}
408+
}

0 commit comments

Comments
 (0)