1- use std:: { cmp:: Ordering , default:: Default } ;
1+ use std:: { cmp:: Ordering , collections :: BTreeSet , default:: Default } ;
22
33use egui:: { Label , Response , Sense , Widget , text:: LayoutJob } ;
44use egui_extras:: TableRow ;
@@ -24,6 +24,12 @@ use crate::views::{
2424pub struct FunctionViewState {
2525 left_highlight : HighlightKind ,
2626 right_highlight : HighlightKind ,
27+ /// Selected row indices for the left column
28+ pub left_selected_rows : BTreeSet < usize > ,
29+ /// Selected row indices for the right column
30+ pub right_selected_rows : BTreeSet < usize > ,
31+ /// Last clicked row index for shift-click range selection
32+ last_selected_row : Option < ( usize , usize ) > , // (column, row_index)
2733}
2834
2935impl FunctionViewState {
@@ -69,6 +75,93 @@ impl FunctionViewState {
6975 self . left_highlight = HighlightKind :: None ;
7076 self . right_highlight = HighlightKind :: None ;
7177 }
78+
79+ /// Get selected rows for a column
80+ pub fn selected_rows ( & self , column : usize ) -> & BTreeSet < usize > {
81+ match column {
82+ 0 => & self . left_selected_rows ,
83+ 1 => & self . right_selected_rows ,
84+ _ => & self . left_selected_rows , // fallback
85+ }
86+ }
87+
88+ /// Check if a row is selected in a column
89+ pub fn is_row_selected ( & self , column : usize , row_index : usize ) -> bool {
90+ match column {
91+ 0 => self . left_selected_rows . contains ( & row_index) ,
92+ 1 => self . right_selected_rows . contains ( & row_index) ,
93+ _ => false ,
94+ }
95+ }
96+
97+ /// Toggle selection of a single row
98+ pub fn toggle_row_selection ( & mut self , column : usize , row_index : usize , shift_held : bool ) {
99+ let selected_rows = match column {
100+ 0 => & mut self . left_selected_rows ,
101+ 1 => & mut self . right_selected_rows ,
102+ _ => return ,
103+ } ;
104+
105+ if shift_held {
106+ // Range selection: select all rows between last selected and current
107+ if let Some ( ( last_col, last_row) ) = self . last_selected_row {
108+ if last_col == column {
109+ let start = last_row. min ( row_index) ;
110+ let end = last_row. max ( row_index) ;
111+ for i in start..=end {
112+ selected_rows. insert ( i) ;
113+ }
114+ } else {
115+ // Different column, just toggle the current row
116+ if selected_rows. contains ( & row_index) {
117+ selected_rows. remove ( & row_index) ;
118+ } else {
119+ selected_rows. insert ( row_index) ;
120+ }
121+ }
122+ } else {
123+ // No previous selection, just select the current row
124+ selected_rows. insert ( row_index) ;
125+ }
126+ } else {
127+ // Single toggle
128+ if selected_rows. contains ( & row_index) {
129+ selected_rows. remove ( & row_index) ;
130+ } else {
131+ selected_rows. insert ( row_index) ;
132+ }
133+ }
134+
135+ self . last_selected_row = Some ( ( column, row_index) ) ;
136+ }
137+
138+ /// Clear all row selections for a column
139+ pub fn clear_row_selection ( & mut self , column : usize ) {
140+ match column {
141+ 0 => self . left_selected_rows . clear ( ) ,
142+ 1 => self . right_selected_rows . clear ( ) ,
143+ _ => { }
144+ }
145+ if self . last_selected_row . map_or ( false , |( col, _) | col == column) {
146+ self . last_selected_row = None ;
147+ }
148+ }
149+
150+ /// Clear all row selections for all columns
151+ pub fn clear_all_row_selections ( & mut self ) {
152+ self . left_selected_rows . clear ( ) ;
153+ self . right_selected_rows . clear ( ) ;
154+ self . last_selected_row = None ;
155+ }
156+
157+ /// Check if any rows are selected in a column
158+ pub fn has_selected_rows ( & self , column : usize ) -> bool {
159+ match column {
160+ 0 => !self . left_selected_rows . is_empty ( ) ,
161+ 1 => !self . right_selected_rows . is_empty ( ) ,
162+ _ => false ,
163+ }
164+ }
72165}
73166
74167fn ins_hover_ui (
@@ -109,10 +202,26 @@ fn ins_context_menu(
109202 column : usize ,
110203 diff_config : & DiffObjConfig ,
111204 appearance : & Appearance ,
112- ) {
205+ has_selection : bool ,
206+ ) -> Option < DiffViewAction > {
207+ let mut ret = None ;
208+
209+ // Add copy/clear selection options if there are selections
210+ if has_selection {
211+ if ui. button ( "📋 Copy selected rows" ) . clicked ( ) {
212+ ret = Some ( DiffViewAction :: CopySelectedRows ( column) ) ;
213+ ui. close ( ) ;
214+ }
215+ if ui. button ( "✖ Clear selection" ) . clicked ( ) {
216+ ret = Some ( DiffViewAction :: ClearRowSelection ( column) ) ;
217+ ui. close ( ) ;
218+ }
219+ ui. separator ( ) ;
220+ }
221+
113222 let Some ( resolved) = obj. resolve_instruction_ref ( symbol_idx, ins_ref) else {
114223 ui. colored_label ( appearance. delete_color , "Failed to resolve instruction" ) ;
115- return ;
224+ return ret ;
116225 } ;
117226 let ins = match obj. arch . process_instruction ( resolved, diff_config) {
118227 Ok ( ins) => ins,
@@ -121,15 +230,19 @@ fn ins_context_menu(
121230 appearance. delete_color ,
122231 format ! ( "Failed to process instruction: {e}" ) ,
123232 ) ;
124- return ;
233+ return ret ;
125234 }
126235 } ;
127236
128237 ui. scope ( |ui| {
129238 ui. style_mut ( ) . override_text_style = Some ( egui:: TextStyle :: Monospace ) ;
130239 ui. style_mut ( ) . wrap_mode = Some ( egui:: TextWrapMode :: Truncate ) ;
131- context_menu_items_ui ( ui, instruction_context ( obj, resolved, & ins) , column, appearance) ;
240+ if let Some ( action) = context_menu_items_ui ( ui, instruction_context ( obj, resolved, & ins) , column, appearance) {
241+ ret = Some ( action) ;
242+ }
132243 } ) ;
244+
245+ ret
133246}
134247
135248#[ must_use]
@@ -209,14 +322,25 @@ fn asm_row_ui(
209322 ins_view_state : & FunctionViewState ,
210323 diff_config : & DiffObjConfig ,
211324 column : usize ,
325+ row_index : usize ,
212326 response_cb : impl Fn ( Response ) -> Response ,
213327) -> Option < DiffViewAction > {
214328 let mut ret = None ;
215329 ui. spacing_mut ( ) . item_spacing . x = 0.0 ;
216330 ui. style_mut ( ) . wrap_mode = Some ( egui:: TextWrapMode :: Extend ) ;
217- if ins_diff. kind != InstructionDiffKind :: None {
331+
332+ // Show selection highlight
333+ let is_selected = ins_view_state. is_row_selected ( column, row_index) ;
334+ if is_selected {
335+ ui. painter ( ) . rect_filled (
336+ ui. available_rect_before_wrap ( ) ,
337+ 0.0 ,
338+ appearance. highlight_color . gamma_multiply ( 0.3 ) ,
339+ ) ;
340+ } else if ins_diff. kind != InstructionDiffKind :: None {
218341 ui. painter ( ) . rect_filled ( ui. available_rect_before_wrap ( ) , 0.0 , ui. visuals ( ) . faint_bg_color ) ;
219342 }
343+
220344 let space_width = ui. fonts_mut ( |f| f. glyph_width ( & appearance. code_font , ' ' ) ) ;
221345 display_row ( obj, symbol_idx, ins_diff, diff_config, |segment| {
222346 if let Some ( action) =
@@ -241,19 +365,10 @@ pub(crate) fn asm_col_ui(
241365) -> Option < DiffViewAction > {
242366 let mut ret = None ;
243367 let symbol_ref = ctx. symbol_ref ?;
244- let ins_row = & ctx. diff . symbols [ symbol_ref] . instruction_rows [ row. index ( ) ] ;
245- let response_cb = |response : Response | {
246- if let Some ( ins_ref) = ins_row. ins_ref {
247- response. context_menu ( |ui| {
248- ins_context_menu ( ui, ctx. obj , symbol_ref, ins_ref, column, diff_config, appearance)
249- } ) ;
250- response. on_hover_ui_at_pointer ( |ui| {
251- ins_hover_ui ( ui, ctx. obj , symbol_ref, ins_ref, diff_config, appearance)
252- } )
253- } else {
254- response
255- }
256- } ;
368+ let row_index = row. index ( ) ;
369+ let ins_row = & ctx. diff . symbols [ symbol_ref] . instruction_rows [ row_index] ;
370+ let has_selection = ins_view_state. has_selected_rows ( column) ;
371+
257372 let ( _, response) = row. col ( |ui| {
258373 if let Some ( action) = asm_row_ui (
259374 ui,
@@ -264,12 +379,47 @@ pub(crate) fn asm_col_ui(
264379 ins_view_state,
265380 diff_config,
266381 column,
267- response_cb,
382+ row_index,
383+ |r| r, // Simple passthrough
268384 ) {
269385 ret = Some ( action) ;
270386 }
271387 } ) ;
272- response_cb ( response) ;
388+
389+ // Handle context menu
390+ if let Some ( ins_ref) = ins_row. ins_ref {
391+ response. context_menu ( |ui| {
392+ if let Some ( action) = ins_context_menu (
393+ ui, ctx. obj , symbol_ref, ins_ref, column, diff_config, appearance, has_selection,
394+ ) {
395+ ret = Some ( action) ;
396+ }
397+ } ) ;
398+ response. on_hover_ui_at_pointer ( |ui| {
399+ ins_hover_ui ( ui, ctx. obj , symbol_ref, ins_ref, diff_config, appearance)
400+ } ) ;
401+ } else if has_selection {
402+ // Even rows without instructions can have context menu for copy/clear selected
403+ response. context_menu ( |ui| {
404+ if ui. button ( "📋 Copy selected rows" ) . clicked ( ) {
405+ ret = Some ( DiffViewAction :: CopySelectedRows ( column) ) ;
406+ ui. close ( ) ;
407+ }
408+ if ui. button ( "✖ Clear selection" ) . clicked ( ) {
409+ ret = Some ( DiffViewAction :: ClearRowSelection ( column) ) ;
410+ ui. close ( ) ;
411+ }
412+ } ) ;
413+ }
414+
415+ // Handle Ctrl+Click for row selection toggle
416+ if response. clicked ( ) {
417+ let modifiers = response. ctx . input ( |i| i. modifiers ) ;
418+ if modifiers. ctrl || modifiers. command {
419+ ret = Some ( DiffViewAction :: ToggleRowSelection ( column, row_index, modifiers. shift ) ) ;
420+ }
421+ }
422+
273423 ret
274424}
275425
0 commit comments