Skip to content

Commit 176b2aa

Browse files
committed
Init
1 parent 51c3af2 commit 176b2aa

File tree

3 files changed

+277
-23
lines changed

3 files changed

+277
-23
lines changed

objdiff-gui/src/views/diff.rs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::cmp::Ordering;
1+
use std::{cmp::Ordering, collections::BTreeSet};
22

33
use egui::{Id, Layout, RichText, ScrollArea, TextEdit, Ui, Widget, text::LayoutJob};
44
use objdiff_core::{
@@ -115,6 +115,55 @@ fn get_asm_text(
115115
asm_text
116116
}
117117

118+
/// Obtains the assembly text for selected rows only, suitable for copying to clipboard.
119+
pub fn get_selected_asm_text(
120+
obj: &Object,
121+
symbol_diff: &SymbolDiff,
122+
symbol_idx: usize,
123+
diff_config: &DiffObjConfig,
124+
selected_rows: &BTreeSet<usize>,
125+
) -> String {
126+
let mut asm_text = String::new();
127+
128+
for (row_idx, ins_row) in symbol_diff.instruction_rows.iter().enumerate() {
129+
if !selected_rows.contains(&row_idx) {
130+
continue;
131+
}
132+
let mut line = String::new();
133+
let result = display_row(obj, symbol_idx, ins_row, diff_config, |segment| {
134+
let text = match segment.text {
135+
DiffText::Basic(text) => text.to_string(),
136+
DiffText::Line(num) => format!("{num} "),
137+
DiffText::Address(addr) => format!("{addr:x}:"),
138+
DiffText::Opcode(mnemonic, _op) => format!("{mnemonic} "),
139+
DiffText::Argument(arg) => match arg {
140+
InstructionArgValue::Signed(v) => format!("{:#x}", ReallySigned(v)),
141+
InstructionArgValue::Unsigned(v) => format!("{v:#x}"),
142+
InstructionArgValue::Opaque(v) => v.into_owned(),
143+
},
144+
DiffText::BranchDest(addr) => format!("{addr:x}"),
145+
DiffText::Symbol(sym) => sym.demangled_name.as_ref().unwrap_or(&sym.name).clone(),
146+
DiffText::Addend(addend) => match addend.cmp(&0i64) {
147+
Ordering::Greater => format!("+{addend:#x}"),
148+
Ordering::Less => format!("-{:#x}", -addend),
149+
_ => String::new(),
150+
},
151+
DiffText::Spacing(n) => " ".repeat(n.into()),
152+
DiffText::Eol => "\n".to_string(),
153+
};
154+
line.push_str(&text);
155+
Ok(())
156+
});
157+
158+
if result.is_ok() {
159+
asm_text.push_str(line.trim_end());
160+
asm_text.push('\n');
161+
}
162+
}
163+
164+
asm_text
165+
}
166+
118167
#[must_use]
119168
pub fn diff_view_ui(
120169
ui: &mut Ui,

objdiff-gui/src/views/function_diff.rs

Lines changed: 171 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::{cmp::Ordering, default::Default};
1+
use std::{cmp::Ordering, collections::BTreeSet, default::Default};
22

33
use egui::{Label, Response, Sense, Widget, text::LayoutJob};
44
use egui_extras::TableRow;
@@ -24,6 +24,12 @@ use crate::views::{
2424
pub 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

2935
impl 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

74167
fn 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

Comments
 (0)