use std::default::Default; use egui::{text::LayoutJob, Align, Label, Layout, Sense, Vec2, Widget}; use egui_extras::{Column, TableBuilder, TableRow}; use objdiff_core::{ diff::display::{display_diff, DiffText, HighlightKind}, obj::{ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjInsDiff, ObjInsDiffKind, ObjSymbol}, }; use time::format_description; use crate::views::{ appearance::Appearance, symbol_diff::{match_color_for_symbol, DiffViewState, SymbolReference, View}, }; #[derive(Default)] pub struct FunctionViewState { pub highlight: HighlightKind, } fn ins_hover_ui(ui: &mut egui::Ui, ins: &ObjIns, appearance: &Appearance) { ui.scope(|ui| { ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().wrap = Some(false); ui.label(format!("{:02X?}", ins.code.to_be_bytes())); if let Some(orig) = &ins.orig { ui.label(format!("Original: {}", orig)); } for arg in &ins.args { if let ObjInsArg::Arg(arg) | ObjInsArg::ArgWithBase(arg) = arg { match arg { ObjInsArgValue::Signed(v) => { ui.label(format!("{arg} == {v}")); } ObjInsArgValue::Unsigned(v) => { ui.label(format!("{arg} == {v}")); } _ => {} } } } if let Some(reloc) = &ins.reloc { ui.label(format!("Relocation type: {:?}", reloc.kind)); ui.colored_label(appearance.highlight_color, format!("Name: {}", reloc.target.name)); if let Some(section) = &reloc.target_section { ui.colored_label(appearance.highlight_color, format!("Section: {section}")); ui.colored_label( appearance.highlight_color, format!("Address: {:x}", reloc.target.address), ); ui.colored_label( appearance.highlight_color, format!("Size: {:x}", reloc.target.size), ); } else { ui.colored_label(appearance.highlight_color, "Extern".to_string()); } } }); } fn ins_context_menu(ui: &mut egui::Ui, ins: &ObjIns) { ui.scope(|ui| { ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().wrap = Some(false); // if ui.button("Copy hex").clicked() {} for arg in &ins.args { if let ObjInsArg::Arg(arg) | ObjInsArg::ArgWithBase(arg) = arg { match arg { ObjInsArgValue::Signed(v) => { if ui.button(format!("Copy \"{arg}\"")).clicked() { ui.output_mut(|output| output.copied_text = arg.to_string()); ui.close_menu(); } if ui.button(format!("Copy \"{v}\"")).clicked() { ui.output_mut(|output| output.copied_text = v.to_string()); ui.close_menu(); } } ObjInsArgValue::Unsigned(v) => { if ui.button(format!("Copy \"{arg}\"")).clicked() { ui.output_mut(|output| output.copied_text = arg.to_string()); ui.close_menu(); } if ui.button(format!("Copy \"{v}\"")).clicked() { ui.output_mut(|output| output.copied_text = v.to_string()); ui.close_menu(); } } _ => {} } } } if let Some(reloc) = &ins.reloc { if let Some(name) = &reloc.target.demangled_name { if ui.button(format!("Copy \"{name}\"")).clicked() { ui.output_mut(|output| output.copied_text = name.clone()); ui.close_menu(); } } if ui.button(format!("Copy \"{}\"", reloc.target.name)).clicked() { ui.output_mut(|output| output.copied_text = reloc.target.name.clone()); ui.close_menu(); } } }); } fn find_symbol<'a>(obj: &'a ObjInfo, selected_symbol: &SymbolReference) -> Option<&'a ObjSymbol> { obj.sections.iter().find_map(|section| { section.symbols.iter().find(|symbol| symbol.name == selected_symbol.symbol_name) }) } fn diff_text_ui( ui: &mut egui::Ui, text: DiffText<'_>, ins_diff: &ObjInsDiff, appearance: &Appearance, ins_view_state: &mut FunctionViewState, space_width: f32, ) { let label_text; let mut base_color = match ins_diff.kind { ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => { appearance.text_color } ObjInsDiffKind::Replace => appearance.replace_color, ObjInsDiffKind::Delete => appearance.delete_color, ObjInsDiffKind::Insert => appearance.insert_color, }; let mut pad_to = 0; match text { DiffText::Basic(text) => { label_text = text.to_string(); } DiffText::BasicColor(s, idx) => { label_text = s.to_string(); base_color = appearance.diff_colors[idx % appearance.diff_colors.len()]; } DiffText::Line(num) => { label_text = num.to_string(); base_color = appearance.deemphasized_text_color; pad_to = 5; } DiffText::Address(addr) => { label_text = format!("{:x}:", addr); pad_to = 5; } DiffText::Opcode(mnemonic, _op) => { label_text = mnemonic.to_string(); if ins_diff.kind == ObjInsDiffKind::OpMismatch { base_color = appearance.replace_color; } pad_to = 8; } DiffText::Argument(arg, diff) => { label_text = arg.to_string(); if let Some(diff) = diff { base_color = appearance.diff_colors[diff.idx % appearance.diff_colors.len()] } } DiffText::BranchTarget(addr) => { label_text = format!("{addr:x}"); } DiffText::Symbol(sym) => { let name = sym.demangled_name.as_ref().unwrap_or(&sym.name); label_text = name.clone(); base_color = appearance.emphasized_text_color; } DiffText::Spacing(n) => { ui.add_space(n as f32 * space_width); return; } DiffText::Eol => { label_text = "\n".to_string(); } } let len = label_text.len(); let highlight = ins_view_state.highlight == text; let response = Label::new(LayoutJob::single_section( label_text, appearance.code_text_format(base_color, highlight), )) .sense(Sense::click()) .ui(ui); response.context_menu(|ui| ins_context_menu(ui, ins_diff.ins.as_ref().unwrap())); if response.clicked() { if highlight { ins_view_state.highlight = HighlightKind::None; } else { ins_view_state.highlight = text.into(); } } if len < pad_to { ui.add_space((pad_to - len) as f32 * space_width); } } fn asm_row_ui( ui: &mut egui::Ui, ins_diff: &ObjInsDiff, symbol: &ObjSymbol, appearance: &Appearance, ins_view_state: &mut FunctionViewState, ) { ui.spacing_mut().item_spacing.x = 0.0; if ins_diff.kind != ObjInsDiffKind::None { ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color); } let space_width = ui.fonts(|f| f.glyph_width(&appearance.code_font, ' ')); display_diff(ins_diff, symbol.address as u32, |text| { diff_text_ui(ui, text, ins_diff, appearance, ins_view_state, space_width); Ok::<_, ()>(()) }) .unwrap(); } fn asm_col_ui( row: &mut TableRow<'_, '_>, ins_diff: &ObjInsDiff, symbol: &ObjSymbol, appearance: &Appearance, ins_view_state: &mut FunctionViewState, ) { let (_, response) = row.col(|ui| { asm_row_ui(ui, ins_diff, symbol, appearance, ins_view_state); }); if let Some(ins) = &ins_diff.ins { response.on_hover_ui_at_pointer(|ui| ins_hover_ui(ui, ins, appearance)); } } fn empty_col_ui(row: &mut TableRow<'_, '_>) { row.col(|ui| { ui.label(""); }); } fn asm_table_ui( table: TableBuilder<'_>, left_obj: Option<&ObjInfo>, right_obj: Option<&ObjInfo>, selected_symbol: &SymbolReference, appearance: &Appearance, ins_view_state: &mut FunctionViewState, ) -> Option<()> { let left_symbol = left_obj.and_then(|obj| find_symbol(obj, selected_symbol)); let right_symbol = right_obj.and_then(|obj| find_symbol(obj, selected_symbol)); let instructions_len = left_symbol.or(right_symbol).map(|s| s.instructions.len())?; table.body(|body| { body.rows(appearance.code_font.size, instructions_len, |mut row| { let row_index = row.index(); if let Some(symbol) = left_symbol { asm_col_ui( &mut row, &symbol.instructions[row_index], symbol, appearance, ins_view_state, ); } else { empty_col_ui(&mut row); } if let Some(symbol) = right_symbol { asm_col_ui( &mut row, &symbol.instructions[row_index], symbol, appearance, ins_view_state, ); } else { empty_col_ui(&mut row); } }); }); Some(()) } pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) { let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol) else { return; }; // Header let available_width = ui.available_width(); let column_width = available_width / 2.0; ui.allocate_ui_with_layout( Vec2 { x: available_width, y: 100.0 }, Layout::left_to_right(Align::Min), |ui| { // Left column ui.allocate_ui_with_layout( Vec2 { x: column_width, y: 100.0 }, Layout::top_down(Align::Min), |ui| { ui.set_width(column_width); ui.horizontal(|ui| { if ui.button("⏴ Back").clicked() { state.current_view = View::SymbolDiff; } ui.separator(); if ui .add_enabled( !state.scratch_running && state.scratch_available, egui::Button::new("📲 decomp.me"), ) .on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)") .on_disabled_hover_text("Scratch configuration missing") .clicked() { state.queue_scratch = true; } }); let name = selected_symbol .demangled_symbol_name .as_deref() .unwrap_or(&selected_symbol.symbol_name); let mut job = LayoutJob::simple( name.to_string(), appearance.code_font.clone(), appearance.highlight_color, column_width, ); job.wrap.break_anywhere = true; job.wrap.max_rows = 1; ui.label(job); ui.scope(|ui| { ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.label("Diff target:"); }); }, ); // Right column ui.allocate_ui_with_layout( Vec2 { x: column_width, y: 100.0 }, Layout::top_down(Align::Min), |ui| { ui.set_width(column_width); ui.horizontal(|ui| { if ui .add_enabled(!state.build_running, egui::Button::new("Build")) .clicked() { state.queue_build = true; } ui.scope(|ui| { ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().wrap = Some(false); if state.build_running { ui.colored_label(appearance.replace_color, "Building…"); } else { ui.label("Last built:"); let format = format_description::parse("[hour]:[minute]:[second]").unwrap(); ui.label( result .time .to_offset(appearance.utc_offset) .format(&format) .unwrap(), ); } }); }); ui.scope(|ui| { ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); if let Some(match_percent) = result .second_obj .as_ref() .and_then(|obj| find_symbol(obj, selected_symbol)) .and_then(|symbol| symbol.match_percent) { ui.colored_label( match_color_for_symbol(match_percent, appearance), &format!("{match_percent:.0}%"), ); } else { ui.colored_label(appearance.replace_color, "Missing"); } ui.label("Diff base:"); }); }, ); }, ); ui.separator(); // Table ui.style_mut().interaction.selectable_labels = false; let available_height = ui.available_height(); let table = TableBuilder::new(ui) .striped(false) .cell_layout(Layout::left_to_right(Align::Min)) .columns(Column::exact(column_width).clip(true), 2) .resizable(false) .auto_shrink([false, false]) .min_scrolled_height(available_height); asm_table_ui( table, result.first_obj.as_ref(), result.second_obj.as_ref(), selected_symbol, appearance, &mut state.function_state, ); }