@@ -16,11 +16,9 @@ use ratatui::widgets::{Paragraph, Wrap};
1616
1717use atto_ui:: app:: {
1818 AppControl , CrosstermAppConfig , CursorMode , Desktop , MenuBar , MenuItem , MenuSpec ,
19- run_crossterm_desktop_with_actions,
20- } ;
21- use atto_ui:: composable:: {
22- Component , ComponentContext , EventHandling , EventResult , FocusNav , Layout , Scrollable ,
19+ popup_menu_window, run_crossterm_desktop_with_actions,
2320} ;
21+ use atto_ui:: composable:: { Component , ComponentContext } ;
2422use atto_ui:: reactive:: { Binding , DirtyObserver } ;
2523use atto_ui:: theme:: Theme ;
2624use atto_ui:: wm:: { Window , WindowId , WindowKind , WindowState } ;
@@ -93,91 +91,6 @@ struct CommandContextState {
9391 block_index : usize ,
9492}
9593
96- struct CommandContextMenuView {
97- action_tx : mpsc:: Sender < TerminalViewerAction > ,
98- last_area : Option < Rect > ,
99- }
100-
101- impl CommandContextMenuView {
102- fn new ( action_tx : mpsc:: Sender < TerminalViewerAction > ) -> Self {
103- Self {
104- action_tx,
105- last_area : None ,
106- }
107- }
108-
109- fn action_for_row ( row : u16 ) -> Option < CommandContextMenuAction > {
110- match row {
111- 0 => Some ( CommandContextMenuAction :: Rerun ) ,
112- 1 => Some ( CommandContextMenuAction :: CopyCommand ) ,
113- 2 => Some ( CommandContextMenuAction :: CopyOutput ) ,
114- _ => None ,
115- }
116- }
117- }
118-
119- impl Component for CommandContextMenuView {
120- fn draw ( & mut self , frame : & mut Frame < ' _ > , area : Rect , _ctx : ComponentContext < ' _ > ) {
121- self . last_area = Some ( area) ;
122- frame. render_widget (
123- Paragraph :: new ( vec ! [
124- Line :: from( "Rerun" ) ,
125- Line :: from( "Copy command" ) ,
126- Line :: from( "Copy output" ) ,
127- ] ) ,
128- area,
129- ) ;
130- }
131- }
132-
133- impl EventHandling for CommandContextMenuView {
134- fn handle_event ( & mut self , event : & Event , ctx : ComponentContext < ' _ > ) -> EventResult {
135- let Event :: Mouse ( MouseEvent {
136- kind : MouseEventKind :: Down ( MouseButton :: Left ) ,
137- column,
138- row,
139- ..
140- } ) = event
141- else {
142- return EventResult :: ignored ( ) ;
143- } ;
144- let Some ( area) = self . last_area else {
145- return EventResult :: ignored ( ) ;
146- } ;
147- let ( local_col, local_row) = match ctx. mouse_coordinate_space {
148- atto_ui:: composable:: MouseCoordinateSpace :: Absolute => {
149- if * column < area. x
150- || * row < area. y
151- || * column >= area. x . saturating_add ( area. width )
152- || * row >= area. y . saturating_add ( area. height )
153- {
154- return EventResult :: ignored ( ) ;
155- }
156- (
157- ( * column) . saturating_sub ( area. x ) ,
158- ( * row) . saturating_sub ( area. y ) ,
159- )
160- }
161- atto_ui:: composable:: MouseCoordinateSpace :: Local => ( * column, * row) ,
162- } ;
163- if local_col >= area. width {
164- return EventResult :: ignored ( ) ;
165- }
166- let Some ( action) = Self :: action_for_row ( local_row) else {
167- return EventResult :: ignored ( ) ;
168- } ;
169- let _ = self
170- . action_tx
171- . send ( TerminalViewerAction :: CommandContext ( action) ) ;
172- EventResult :: consumed ( )
173- }
174- }
175-
176- impl Layout for CommandContextMenuView { }
177- impl Scrollable for CommandContextMenuView { }
178- impl FocusNav for CommandContextMenuView { }
179- atto_ui:: impl_component_default_traits!( CommandContextMenuView => DynamicTree ) ;
180-
18194struct FeatureGuideView {
18295 lines : Vec < String > ,
18396}
@@ -406,7 +319,6 @@ fn seed_terminal_banner(
406319 handle. process_output_str (
407320 "Right-click an OSC 133 command block for rerun/copy actions when shell integration is active.\r \n " ,
408321 ) ;
409- handle. process_output_str ( "\x1b [?1000h\x1b [?1006h" ) ;
410322 Ok ( ( ) )
411323}
412324
@@ -653,15 +565,26 @@ fn command_block_at_mouse(
653565 None
654566}
655567
656- fn command_context_menu_rect ( screen : Rect , mouse : & MouseEvent ) -> Rect {
657- let width = 18 . min ( screen. width . max ( 1 ) ) ;
658- let height = 5 . min ( screen. height . max ( 1 ) ) ;
659- Rect {
660- x : mouse. column . min ( screen. width . saturating_sub ( width) ) ,
661- y : mouse. row . min ( screen. height . saturating_sub ( height) ) ,
662- width,
663- height,
664- }
568+ fn command_context_menu_items ( action_tx : & mpsc:: Sender < TerminalViewerAction > ) -> Vec < MenuItem > {
569+ let send = |action : CommandContextMenuAction , tx : mpsc:: Sender < TerminalViewerAction > | {
570+ move || {
571+ let _ = tx. send ( TerminalViewerAction :: CommandContext ( action) ) ;
572+ }
573+ } ;
574+ vec ! [
575+ MenuItem :: action(
576+ "Rerun" ,
577+ send( CommandContextMenuAction :: Rerun , action_tx. clone( ) ) ,
578+ ) ,
579+ MenuItem :: action(
580+ "Copy command" ,
581+ send( CommandContextMenuAction :: CopyCommand , action_tx. clone( ) ) ,
582+ ) ,
583+ MenuItem :: action(
584+ "Copy output" ,
585+ send( CommandContextMenuAction :: CopyOutput , action_tx. clone( ) ) ,
586+ ) ,
587+ ]
665588}
666589
667590fn open_command_context_menu (
@@ -688,13 +611,12 @@ fn open_command_context_menu(
688611 let _ = pane. handle . select_command_block_output ( block_index) ;
689612 }
690613 let menu_id = desktop. add_window (
691- Window :: new (
692- WindowKind :: Tooltip ,
614+ popup_menu_window (
615+ command_context_menu_items ( action_tx) ,
616+ ( mouse. column , mouse. row ) ,
617+ screen,
693618 "Command" ,
694- command_context_menu_rect ( screen, mouse) ,
695- Box :: new ( CommandContextMenuView :: new ( action_tx. clone ( ) ) ) ,
696- )
697- . with_min_size ( 18 , 5 ) ,
619+ ) ,
698620 screen,
699621 ) ;
700622 * context = Some ( CommandContextState {
@@ -754,6 +676,50 @@ fn is_right_mouse_down(event: &Event) -> Option<&MouseEvent> {
754676 }
755677}
756678
679+ fn non_right_mouse_down ( event : & Event ) -> Option < & MouseEvent > {
680+ match event {
681+ Event :: Mouse (
682+ mouse @ MouseEvent {
683+ kind : MouseEventKind :: Down ( MouseButton :: Left | MouseButton :: Middle ) ,
684+ ..
685+ } ,
686+ ) => Some ( mouse) ,
687+ _ => None ,
688+ }
689+ }
690+
691+ /// Dismisses the context menu when a non-right click lands outside the menu
692+ /// window. Clicks inside the menu are handled by the menu view itself (a row
693+ /// activation closes the window and its action is applied next frame, so the
694+ /// stored context must survive this call).
695+ fn dismiss_context_menu_on_outside_click (
696+ desktop : & mut Desktop ,
697+ event : & Event ,
698+ context : & mut Option < CommandContextState > ,
699+ ) {
700+ let Some ( mouse) = non_right_mouse_down ( event) else {
701+ return ;
702+ } ;
703+ let Some ( state) = * context else {
704+ return ;
705+ } ;
706+ // If the menu window is gone, the click already activated an item; keep the
707+ // context so `on_action` can apply it next frame.
708+ let outside = match desktop. wm . window ( state. menu_id ) {
709+ Some ( window) => {
710+ let rect = window. rect . get ( ) ;
711+ mouse. column < rect. x
712+ || mouse. row < rect. y
713+ || mouse. column >= rect. x . saturating_add ( rect. width )
714+ || mouse. row >= rect. y . saturating_add ( rect. height )
715+ }
716+ None => false ,
717+ } ;
718+ if outside {
719+ close_command_context_menu ( desktop, context) ;
720+ }
721+ }
722+
757723fn spawn_terminal_window (
758724 desktop : & mut Desktop ,
759725 screen : Rect ,
@@ -1180,6 +1146,12 @@ fn main() -> Result<()> {
11801146 & action_tx_for_event,
11811147 & mut command_context_for_event. borrow_mut ( ) ,
11821148 ) ;
1149+ } else {
1150+ dismiss_context_menu_on_outside_click (
1151+ desktop,
1152+ ev,
1153+ & mut command_context_for_event. borrow_mut ( ) ,
1154+ ) ;
11831155 }
11841156 if is_plain_restart_key ( ev) {
11851157 let mut sessions = terminal_sessions_for_event. borrow_mut ( ) ;
0 commit comments