@@ -3,14 +3,12 @@ use std::{cell::RefCell, rc::Rc};
33use gpui:: {
44 Anchor , AnyElement , App , Context , DismissEvent , Element , ElementId , Entity , Focusable ,
55 GlobalElementId , Hitbox , HitboxBehavior , InspectorElementId , InteractiveElement , IntoElement ,
6- MouseButton , MouseDownEvent , MouseUpEvent , ParentElement , Pixels , Point , StyleRefinement ,
7- Styled , Subscription , Window , anchored, deferred, div, prelude:: FluentBuilder , px,
6+ MouseButton , MouseDownEvent , ParentElement , Pixels , Point , StyleRefinement , Styled ,
7+ Subscription , Window , anchored, deferred, div, prelude:: FluentBuilder , px,
88} ;
99
1010use crate :: menu:: PopupMenu ;
1111
12- type PopupMenuBuilder = Rc < dyn Fn ( PopupMenu , & mut Window , & mut Context < PopupMenu > ) -> PopupMenu > ;
13-
1412/// A extension trait for adding a context menu to an element.
1513pub trait ContextMenuExt : InteractiveElement + ParentElement + Styled {
1614 /// Add a context menu to the element.
@@ -42,7 +40,7 @@ impl<E: InteractiveElement + ParentElement + Styled> ContextMenuExt for E {}
4240pub struct ContextMenu < E : ParentElement + Styled + Sized > {
4341 id : ElementId ,
4442 element : Option < E > ,
45- menu : Option < PopupMenuBuilder > ,
43+ menu : Option < Rc < dyn Fn ( PopupMenu , & mut Window , & mut Context < PopupMenu > ) -> PopupMenu > > ,
4644 // This is not in use, just for style refinement forwarding.
4745 _ignore_style : StyleRefinement ,
4846 anchor : Anchor ,
@@ -121,57 +119,6 @@ struct ContextMenuSharedState {
121119 _subscription : Option < Subscription > ,
122120}
123121
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-
175122pub struct ContextMenuState {
176123 element : Option < AnyElement > ,
177124 shared_state : Rc < RefCell < ContextMenuSharedState > > ,
@@ -323,86 +270,56 @@ impl<E: ParentElement + Styled + IntoElement + 'static> Element for ContextMenu<
323270 |_view, state : & mut ContextMenuState , window, _| {
324271 let shared_state = state. shared_state . clone ( ) ;
325272
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.
273+ let hitbox = hitbox. clone ( ) ;
274+ // When right mouse click, to build content menu, and show it at the mouse position.
330275 window. on_mouse_event ( move |event : & MouseDownEvent , phase, window, cx| {
331276 if phase. bubble ( )
332277 && event. button == MouseButton :: Right
333- && hitbox_for_down . is_hovered ( window)
278+ && hitbox . is_hovered ( window)
334279 {
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- ) ;
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+ } ) ;
362320 }
363321 } ) ;
364322 } ,
365323 ) ;
366324 }
367325}
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